API
Storage classes
- class alliance_platform.storage.async_uploads.storage.base.AsyncUploadStorage
Base storage class for use with
AsyncFileFieldProvides
generate_upload_urlto return a URL to upload a file to (eg. a signed URL) and amove_filemethod to move a file from a temporary location to the permanent location. The key used for temporary files is returned bygenerate_temporary_pathandis_temporary_pathshould return whether a file is in the temporary location and needs to be moved to a permanent location.See
AsyncFileMixinfor a detailed explanation of how all the pieces fit together.- generate_download_url(key, field_id, **kwargs)
Should return a URL that the specified key should be downloadable from
In S3 this would be a signed URL.
- Parameters:
key (str)
field_id (str)
- generate_temporary_path(filename, max_length=None)
Generates a unique key to upload
filenametoThis generates a string like
async-temp-files/2021/03/03/fVy5cSVBQpOb-test.pngwheretest.pngis thefilenamepassed in. The part after-will be truncated to fit withinmax_lengthbut will retain the file extension. If there is insufficient length to accommodate the temporary path prefix (up to the-and the file extension an error will be thrown.
- generate_upload_url(name, field_id, *args, **kwargs)
Should return a URL that a file can be uploaded directly to
In S3 this would be a signed URL.
- Parameters:
name (str)
field_id (str)
- Return type:
GenerateUploadUrlResponse
- get_url_patterns(registry)
Return the URL patterns for any views required by the storage class
When extending
AsyncUploadStorage, this method can be implemented if any custom views are required to support the implementation. By default, two views are supplied:DownloadRedirectViewto support downloading an existing file. This is attached to the “download-file/” path.GenerateUploadUrlViewto generate a URL that can be uploaded to directly from the frontend. This is attached to the “generate-upload-url/” path.
This method is called by
get_url_patterns().- Parameters:
registry (AsyncFieldRegistry)
- is_temporary_path(filename)
Is the specified path that of a temporary file?
This is used to determine whether a file should be moved to a permanent location. If this returns true it’s expected an AsyncTempFile exists with the matching key.
Default implementation checks if path begins with
temporary_key_prefix- Parameters:
filename (str)
- Return type:
bool
- move_file(from_key, to_key)
Move a file from one location to another
The details of this depend on the storage solution. In S3 this involves copying the file and then deleting the original file.
- temporary_key_prefix = 'async-temp-files'
The prefix to use on files when first uploaded before they are moved. This is used to identify a file as a temporary file to know whether to move them and so must be unique to temporary files.
- class alliance_platform.storage.async_uploads.storage.s3.S3AsyncUploadStorage(*args, **kwargs)
S3 implementation of AsyncUploadStorage
Uses signed URLs for uploading.
- class alliance_platform.storage.async_uploads.storage.azure.AzureAsyncUploadStorage(*args, **kwargs)
Azure implementation of AsyncUploadStorage
Uses signed URLs for uploading.
- class alliance_platform.storage.async_uploads.storage.filesystem.FileSystemAsyncUploadStorage(*args, **kwargs)
Implementation of AsyncUploadStorage that uploads directly to the local server
This is useful in local dev, or when you still want the behaviour of uploading immediately rather than waiting until the whole form is submitted.
To use this by default, set the
STORAGESsetting:STORAGES = { "default": { "BACKEND": "alliance_platform.storage.async_uploads.storage.filesystem.FileSystemAsyncUploadStorage" }, }
Alternatively, pass an instance of the class to the
storageargument onAsyncFileFieldorAsyncImageField.
Staticfiles Storage
- class alliance_platform.storage.staticfiles.storage.ExcludingManifestStaticFilesStorage(*args, **kwargs)
Custom static files storage that excludes specified files from being processed by
ManifestStaticFilesStorage(i.e., they won’t have their names hashed). This is useful for files that are already hashed, or are never cached by the browser.To use this class, set it in the
STORAGESsetting. Pass theexclude_patternsoption as shown below. This can be set to patterns accepted byfnmatch()Note
The patterns accepted are not the same as regular expressions - see
fnmatchfor details.Usage:
STORAGES = { "staticfiles": { "BACKEND": "alliance_platform.storage.staticfiles.storage.ExcludingManifestStaticFilesStorage", "OPTIONS": {"exclude_patterns": ["frontend/build/*", "*.pdf"]}, } }
Models
- class alliance_platform.storage.async_uploads.models.AsyncTempFile(*args, **kwargs)
Model to track files that are being uploaded to a temporary location
GenerateUploadUrlViewis used to generate a URL to directly upload a file to. When this URL is generated anAsyncTempFileis created to track the new key that is used (eg. /temp/2020/01/04/abc123-myfile.png), the original filename (eg. myfile.png) and the specific field it came from (this is done viaalliance_platform.storage.async_uploads.registry.AsyncFieldRegistry).Once a file has been uploaded and the form saved the key recorded here will be saved against the underlying file field (either
AsyncFileFieldorAsyncImageField) on the target model which will check if that key is a temporary file using anis_temporary_path(). If so the file will be moved to its permanent location usingmove_file()and theAsyncTempFilerecord will have themoved_to_locationvalue set. SeeAsyncFileMixinfor where this happens and more details.You must run the
cleanup_async_temp_filescommand periodically to cleanup this table. This handles two cases: the success case wheremoved_to_locationis set but the record is being kept around for a while to detect duplicate submissions, and the other case where upload occurred on the frontend but the form was never submitted and the file was never moved.
Fields
- class alliance_platform.storage.async_uploads.models.AsyncFileMixin(*args, perm_create=<object object>, perm_update=<object object>, perm_detail=<object object>, max_size=100, file_restrictions=None, async_field_registry=<alliance_platform.storage.async_uploads.registry.AsyncFieldRegistry object>, max_length=500, download_params=None, **kwargs)
Mixin for file fields that works with
AsyncUploadStorageto handle uploading directly to external service like S3 or Azure.This field works in conjunction with
GenerateUploadUrlView. The view will generate a URL (eg. a signed URL when using S3) that the frontend can then upload to. Each view is tied to a specific registry which you can specify inasync_field_registry(defaults todefault_async_field_registry).The permissions used by
GenerateUploadUrlViewcan be specified inperm_createandperm_update. If not provided they default to the value returned byalliance_platform.auth.resolve_perm_name()for the action ‘create’ and ‘update’ respectively. To disable checking permissions, you can passNone- this will mean any user, including anonymous users, can generate upload urls.When using django forms
AsyncFileFieldprovides a widget for handling the upload from the frontend. This is the defaultformfieldprovided byAsyncFileField.Note
The key for the file is stored in the database as a CharField and as such has a max_length. The default for this is
500. This must be sufficient to accommodate the temporary file value which looks something likeasync-temp-files/2021/03/03/fVy5cSVBQpOb-test.pngby default (seegenerate_temporary_path()for how to customise this).Anything after the
-will be truncated if necessary for the purposes of the temporary key. The actual filename will still be used for the final field value after the temporary file is moved to the permanent location. When moving to the permanent location the filename may be truncated if the length is too long whenupload_tois taken into account.If using
S3AsyncUploadStorageyou may wish to setAWS_S3_FILE_OVERWRITE = Falseto avoid overwriting files if the same key is used. This is not necessary if your files generate unique paths (eg.upload_totakes into account the record ID and filename).The flow for how this works is as follows:
When a form is rendered on the frontend (eg. using
AsyncFileField) it knows theasync_field_idfrom the registry andgenerate_upload_urlwhich is to theGenerateUploadUrlViewview for the registry. This URL will be used to generated a specific URL for each file to upload to.When an upload occurs on the frontend it first hits
generate_upload_urland passes theasync_field_id, the filename and optionally aninstance_idif it’s an update for an existing record.GenerateUploadUrlViewlooks up the registry for theasync_field_idto get the field and checks permissions on it (perm_updateifinstance_idis passed otherwiseperm_create). If the permission check passes it will then create aAsyncTempFilerecord to track thefilenamepassed in and agenerated keythat will be used to upload the file to a temporary location on the storage backend. The view will return an upload url by callinggenerate_upload_url()along with the generatedkey.The frontend will receive the
upload_urlandkeyand proceed to upload to it. When the form is submitted and saved thekey(the value returned fromgenerated key) is the what will be stored in the database.On save a
post_savesignal will be called which will check if the file needs to be moved to a permanent location. It does this by callingis_temporary_path()andmove_file()is called to move the file to its permanent location at which point theAsyncTempFileis deleted.If an upload occurs but the form isn’t submitted
AsyncTempFilewill be created but never deleted. Thecleanup_async_temp_filesshould be run periodically to clean these up.
The url returned when accessing the
urlproperty, eg.model.file_field.urlwill always return the URL forDownloadRedirectView. This view will check the user has permission to download the file by checking theperm_downloadpermission. If not provided this defaults to the value returned byalliance_platform.auth.resolve_perm_name()with the action “detail” (ie. if they have permission to view the record they can download the file). This view will then redirect to the URL provided bygenerate_download_url().You can specify
download_paramson fields to control what arguments are passed through togenerate_download_url(). For example:expire_in_seconds = 60 * 60 * 6 download_params = { "expire": expire_in_seconds, "parameters": {"cache_control": f"public,max-age={expire_in_seconds}"}, } # pass to a field on the model image_file = AsyncFileField(download_params=download_params)
Note
You must you this with a storage class that implements
AsyncUploadStorage. Either set DEFAULT_FILE_STORAGE to a class (eg.S3AsyncUploadStorage) or pass an instance in thestoragekwarg.For use with a Presto form use
AsyncFileFieldorAsyncImageFieldon your serializer. This is handled by default when extendingxenopus_frog_app.base.XenopusFrogAppModelSerializer. Codegen will create this as aAsyncFileFieldorAsyncImageFieldon the frontend Presto model.getWidgetForFieldwill map these fields to theUploadWidget.See
AsyncFileFieldandAsyncFileFieldfor Field classes that use this.Warning
User input should be assigned using the
AsyncFileInputDataclass which has validation to disallow things like moving the file by changing the key that passed from the frontend. This is handled for you when usingAsyncFileField,AsyncImageField,AsyncFileFieldorAsyncImageFieldIf a file cannot be moved for any reason (eg. any exception is raised) then the error will be saved in the
errorfield on theAsyncTempFilerecord and the temporary key will be retained as the field value. The error will also be logged to thealliance_platform.storagelogger. This cannot be resolved automatically and so requires manual intervention to cleanup.File size can be restricted by passing in
max_size(in MB)The file extension/file type can be restricted by passing in
file_restrictionsfor example:["image/png"] ["image/*"] [".doc", "docx"] ["application/vnd.openxmlformats-officedocument.wordprocessingml.document"]
- Parameters:
download_params (dict | None)
- class alliance_platform.storage.async_uploads.models.AsyncFileField(*args, perm_create=<object object>, perm_update=<object object>, perm_detail=<object object>, max_size=100, file_restrictions=None, async_field_registry=<alliance_platform.storage.async_uploads.registry.AsyncFieldRegistry object>, max_length=500, download_params=None, **kwargs)
A FileField that works with AsyncUploadStorage to directly upload somewhere (eg. S3) from the frontend
See
AsyncFileMixinfor more details on how this field works.- Parameters:
download_params (dict | None)
- class alliance_platform.storage.async_uploads.models.AsyncImageField(*args, suppress_pillow_check=False, **kwargs)
A ImageField that works with AsyncUploadStorage to directly upload somewhere (eg. S3) from the frontend
This supports
width_fieldandheight_fieldin two ways1) (preferred) The frontend passes the width & height with the form submission. This is supported with the
AsyncImageFieldform field (the default). This works in conjunction with theUploadWidgeton the frontend.2) (slow) Same behaviour as
ImageFieldwhich requires the file to be downloaded and processed with Pillow.See
AsyncFileMixinfor more details on how this field works.
Serializer Fields
To automatically map model fields to serializer fields add this to the base ModelSerializer class (this has already
been done in xenopus_frog_app.base.XenopusFrogAppModelSerializer):
from rest_framework.serializers import ModelSerializer
from alliance_platform.storage.async_uploads.rest_framework import AsyncFileField
from alliance_platform.storage.async_uploads.rest_framework import AsyncImageField
import alliance_platform.storage.async_uploads.models as async_file_fields
class BaseModelSerializer(ModelSerializer):
serializer_field_mapping = {
**ModelSerializer.serializer_field_mapping,
async_file_fields.AsyncFileField: AsyncFileField,
async_file_fields.AsyncImageField: AsyncImageField,
}
- class alliance_platform.storage.async_uploads.rest_framework.AsyncFileField(*args, **kwargs)
Field that works with
AsyncUploadStorageto handle uploading directly to external service like S3 or Azure.Unlike most fields this field must be backed by an underlying model field specified in the
model_fieldkwarg. This is inferred automatically if using a base ModelSerializer class with the following:import alliance_platform.storage.async_uploads.models as async_file_fields class BaseModelSerializer(ModelSerializer): serializer_field_mapping = { **ModelSerializer.serializer_field_mapping, async_file_fields.AsyncFileField: AsyncFileField, async_file_fields.AsyncImageField: AsyncImageField, }
This field expects to receive data in the shape { “key”: “/storage/file.png”, “name”: “file.png” }. It will extract the key to be set on the file field. See
AsyncFileMixinfor more details. ForAsyncImageFieldthewidthandheightkeys may also exist in the data. SeeAsyncFileInputDataSerialized data is in shape { “key”: “/storage/file.png”, “name”: “file.png”, “url”: “/download/?field_id=…” } which is supported by the UploadWidget on the frontend.
- class alliance_platform.storage.async_uploads.rest_framework.AsyncImageField(*args, **kwargs)
Same behaviour as
AsyncFileFieldThis exists as a separate class to allow codegen to differentiate for the purpose of customising the field & widget on the frontend.
Forms
- class alliance_platform.storage.async_uploads.forms.AsyncFileField(*args, async_field_registry, async_field_id, storage, max_length=None, **kwargs)
Form field that renders a
AsyncFileInputThis is the default form field for
alliance_platform.storage.async_uploads.models.AsyncFileField- Parameters:
async_field_registry (AsyncFieldRegistry)
async_field_id (str)
storage (AsyncUploadStorage)
- __init__(*args, async_field_registry, async_field_id, storage, max_length=None, **kwargs)
- Parameters:
*args – Any additional arguments to pass through to
django.forms.Fieldasync_field_registry (AsyncFieldRegistry) – The async field registry that should used on the frontend to create unique upload urls for files. This typically comes from
field.async_field_registrywherefieldis anAsyncFileField.async_field_id (str) – The string ID of the field used in the registry. Generated with
field.async_field_registry.generate_id(field).storage (AsyncUploadStorage) – The storage class. This comes from the field
field.storage.max_length – Max length for the filename
**kwargs – ny additional keyword arguments to pass through to
django.forms.Field
- class alliance_platform.storage.async_uploads.forms.AsyncFileInput(attrs=None)
Input for handling async uploads
This handles the submission value from UploadWidget and converts it to an instance of
AsyncFileInputData. This is then handled on the descriptor classes forAsyncFileFieldandAsyncImageField.To customise the widget rendered on the frontend you can override the template
alliance_platform/storage/widgets/async_file_input.html.
- class alliance_platform.storage.async_uploads.forms.AsyncFileInputData(key, name, width=None, height=None, error=None)
The data we receive from the frontend gets converted to an instance of this
widthandheightcan optionally be included for images to avoid having to calculate dimensions manually when anAsyncImageFielduses thewidth_fieldandheight_fieldoptions.- Parameters:
key (str)
name (str)
width (int | None)
height (int | None)
error (str | None)
- classmethod create_from_user_input(values)
Create instance from user input.
Throws if invalid keys present in
values.urlis accepted but ignored.- Parameters:
values (dict[str, str | int])
- update_dimension_cache(field)
Updates the dimension_cache on
AsyncImageField- Parameters:
field (Field)
- validate_key(field, existing_value)
Validate the key should be accepted
This checks to make sure the key is a temporary path or matches the existing field value. This prevents manipulation of keys which could result in eg. taking over another records file
- Parameters:
field (AsyncFileField)
existing_value (str | AsyncFieldFile | None)
- class alliance_platform.storage.async_uploads.forms.AsyncImageField(*args, async_field_registry, async_field_id, storage, max_length=None, **kwargs)
- Parameters:
async_field_registry (AsyncFieldRegistry)
async_field_id (str)
storage (AsyncUploadStorage)
Views
- class alliance_platform.storage.async_uploads.views.GenerateUploadUrlView(**kwargs)
View to generate a URL using
generate_upload_url().Each view is tied to a specific registry which you can specify in the
registrykwarg (defaults todefault_async_field_registry).This view expects a
async_field_id, afilenameand optionally aninstance_idif it’s an update for an existing record.async_field_idis used to look up inregistrythe matchingfieldinstance. From the retrieved field instance the correspondingAsyncUploadStorageis retrieved along with the permissions to check (seeAsyncFileMixinfor where to specify this).If the permission check passes a
AsyncTempFilerecord is created which stores thefilenameand a generated key that will be used as a temporary location to upload the file to. An upload URL returned fromgenerate_upload_url()and the temporary key is returned to the frontend. The frontend will submit the returned temporary key from the form and save it on the target model. On save the file is then moved to the permanent location usingmove_file()(this happens as part ofAsyncFileMixin)See
AsyncFileMixinfor a detailed explanation of how all the pieces fit together.This view is automatically registered if you have followed the Register URLs guide.
- class alliance_platform.storage.async_uploads.views.DownloadRedirectView(**kwargs)
View that checks permissions and redirects to a download URL using
generate_download_url().Each view is tied to a specific registry which you can specify in the
registrykwarg (defaults todefault_async_field_registry).This view expects a
async_field_idandinstance_idwhich is the primary key of the record the file field is attached to. record.async_field_idis used to look up inregistrythe matchingfieldinstance. From the retrieved field instance the correspondingAsyncUploadStorageis retrieved along with the permission (perm_download) to check (seeAsyncFileMixinfor where to specify this).Once the permission check has passed the view will redirect to the URL returned from
generate_download_url().See
AsyncFileMixinfor a detailed explanation of how all the pieces fit together.This view is automatically registered if you have followed the Register URLs guide.
- class alliance_platform.storage.async_uploads.views.filesystem.FileSystemAsyncStorageUploadView(**kwargs)
View to handle a direct upload to the filesystem when using
FileSystemAsyncUploadStorage.Each view is tied to a specific registry which you can specify in the
registrykwarg (defaults todefault_async_field_registry).Note that this view does works off of signed data generated by
generate_upload_url, and does not itself check any permissions. It does verify the signed data, including enforcing an expiry based onUPLOAD_URL_EXPIRY. This is similar to a signed URL generated by the S3 or Azure backends.The permission checking is done in
GenerateUploadUrlView, which callsgenerate_upload_url.The URL for this view is automatically registered when
FileSystemAsyncUploadStorageis used if you have followed the Register URLs guide.
- class alliance_platform.storage.async_uploads.views.filesystem.FileSystemAsyncStorageDownloadView(**kwargs)
View to handle a direct download from the filesystem when using
FileSystemAsyncUploadStorage.Each view is tied to a specific registry which you can specify in the
registrykwarg (defaults todefault_async_field_registry).Note that this view does works off of signed data generated by
generate_download_url, and does not itself check any permissions. It does verify the signed data, including enforcing an expiry based onDOWNLOAD_URL_EXPIRY. This is similar to a signed URL generated by the S3 or Azure backends.The permission checking is done in
GenerateUploadUrlView, which callsgenerate_upload_url.The URL for this view is automatically registered when
FileSystemAsyncUploadStorageis used if you have followed the Register URLs guide.
Registry
- class alliance_platform.storage.async_uploads.registry.AsyncFieldRegistry(name)
A registry for async fields. This allows looking up a field by its ID.
In most cases the default registry
default_async_field_registryis suitable and more than one registry is not necessary.- generate_id(field)
Generate a string ID for a field to be used to lookup into
fields_by_id.This is passed from the frontend to uniquely identify a field in a registry.
- Parameters:
field (Field)
- get_url_patterns()
Get the required URL patterns for the storage classes used by the registered fields
This calls
get_url_patterns()on each storage instance and combines the results.Usage:
from django.urls import path from django.urls import include from alliance_platform.storage.async_uploads.registry import default_async_field_registry urlpatterns = [path("async-uploads/", include(default_async_field_registry.get_url_patterns()))]
- register_field(field)
Register a field in this registry.
- Parameters:
field (AsyncFileField | AsyncImageField)
- alliance_platform.storage.async_uploads.registry.default_async_field_registry
The default registry that is used when one is not explicitly specified