Bazis Uploadable
An extension package for Bazis, providing a ready-to-use solution for file uploading and management with support for various storage backends.
Quick Start
# Install the package
uv add bazis-uploadable
# Create a model for file uploads
from bazis.contrib.uploadable.models_abstract import FileUploadAbstract
from bazis.core.models_abstract import DtMixin, UuidMixin
class Document(FileUploadAbstract, DtMixin, UuidMixin):
"""Uploadable document"""
class Meta:
verbose_name = 'Document'
verbose_name_plural = 'Documents'
# Create a route for uploads
from bazis.contrib.uploadable.routes import FileUploadRouteSet
from django.apps import apps
class DocumentRouteSet(FileUploadRouteSet):
model = apps.get_model('myapp.Document')
Table of Contents
- Description
- Requirements
- Installation
- Core Components
- Usage
- Storage Configuration
- Examples
- License
- Links
Description
Bazis Uploadable is an extension package for the Bazis framework that provides a ready-to-use solution for file uploading and management. The package includes:
- FileUploadAbstract — abstract model with fields for storing files and metadata
- FileUploadRouteSet — ready-to-use route with multipart/form-data upload support
- Flexible storage configuration — support for local storage, S3, and other backends
- Automatic metadata extraction — filename, extension, size
- JSON:API integration — standardized response format
This package requires bazis, bazis-users and bazis-author (installed as dependencies).
Requirements
- Python: 3.12+
- bazis: latest version
- bazis-author (with bazis-users): the author of a file; set
BS_AUTH_USER_MODEL - PostgreSQL: 12+
- Redis: For caching
Installation
Using uv (recommended)
uv add bazis-uploadable
Using pip
pip install bazis-uploadable
Core Components
FileUploadAbstract
Abstract model for storing uploaded files.
Location: bazis.contrib.uploadable.models_abstract.FileUploadAbstract
Fields:
file— FileField for storing the filename— filename (optional, automatically filled from file name)extension— file extension (optional, automatically extracted)author,author_updated— the user who uploaded the file (AuthorMixinof bazis-author)
Properties:
size— file size in bytes (cached_property)
Features:
- Automatically extracts filename and extension on save
- Uses
get_file_pathto generate storage path - Supports custom storage backends via settings
- Returns 0 for
sizewhen file is absent instead of raising an error
Usage example:
from bazis.contrib.uploadable.models_abstract import FileUploadAbstract
from bazis.core.models_abstract import DtMixin, UuidMixin, JsonApiMixin
class Document(FileUploadAbstract, DtMixin, UuidMixin):
"""Document with additional fields"""
description = models.TextField('Description', blank=True, null=True)
class Meta:
verbose_name = 'Document'
verbose_name_plural = 'Documents'
FileUploadRouteSet
Ready-to-use route for file uploading via multipart/form-data.
Location: bazis.contrib.uploadable.routes.FileUploadRouteSet
Features:
- Accepts files via
multipart/form-data; the server generates the id - Automatically includes
sizeandextensionfields in schema - Supports optional
namefield for renaming files - Returns data in JSON:API format
- Requires a user; the uploader is the
authorof the file
Endpoints:
- POST / — upload a new file
- GET /, GET /{id}/ — the files of the user (others are 404)
Usage example:
from bazis.contrib.uploadable.routes import FileUploadRouteSet
from django.apps import apps
class DocumentRouteSet(FileUploadRouteSet):
model = apps.get_model('myapp.Document')
Storage Configuration
By default, Django's standard FileSystemStorage is used. You can configure custom storage via settings.
settings.py:
# Local storage (default)
BAZIS_STORAGE_FILE_UPLOAD = None # or 'django.core.files.storage.FileSystemStorage'
# S3-compatible storage
BAZIS_STORAGE_FILE_UPLOAD = 'storages.backends.s3boto3.S3Boto3Storage'
# Other storage backends
BAZIS_STORAGE_FILE_UPLOAD = 'path.to.your.CustomStorage'
Size Limit
BAZIS_FILE_UPLOAD_MAX_SIZE (bytes, 0 — no limit, the default) rejects larger files with
413 and the error code ERR_FILE_TOO_LARGE. The check runs after the request body is
received, so also limit the request size on the reverse proxy (e.g. client_max_body_size).
Access Control
FileUploadRouteSet requires a logged-in user (bazis-users): the user who uploads a file is
its author, and the list and retrieve show a user only his own files. Files are not
changed or deleted through the API. Files without an author (uploaded before 2.5) are not
shown to anyone.
The same rule holds for the other routes (Bazis 2.7): FileUploadRouteSet is the default
route of the files, and its restrict_queryset is what a relationship of another model
links and what included shows. A user who sets relationships.<field> = {"id": <file id>}
to a file of another user gets 403 with the error code ERR_RELATION_ACCESS, and
?include=<field> leaves out the files of other users (the relationship keeps the
identifier). The route sets of the models that reference uploaded files need no checks of
their own (the sample route set notes.routes.NoteRouteSet has none). To share the
attachments of shared objects, a project widens restrict_queryset in a subclass with
default_route = True; see bazis/contrib/uploadable/AGENTS.md.
Serving Uploaded Files
An uploaded HTML page or SVG image runs its scripts when it is opened from the media host.
Serve the media from a separate origin (never the origin of the frontend or the API: the
scripts could read the session token in its localStorage), show only raster images
inline, and send every other file with Content-Disposition: attachment and
X-Content-Type-Options: nosniff. An S3 storage of django-storages writes each object
with the type of its name (not the one the client declared) and, unless it is a raster
image, with Content-Disposition: attachment automatically; nosniff is a header of the
CDN or proxy in front of the bucket. For the file system storage configure the web server
of the media host (an nginx example is in bazis/contrib/uploadable/AGENTS.md). The media URL of a file is readable by
anyone who has it: use signed URLs for private files.
Usage
Creating a File Model
from bazis.contrib.uploadable.models_abstract import FileUploadAbstract
from bazis.core.models_abstract import DtMixin, UuidMixin, JsonApiMixin
from django.db import models
class Document(FileUploadAbstract, DtMixin, UuidMixin):
"""Document with metadata"""
description = models.TextField('Description', blank=True, null=True)
category = models.CharField('Category', max_length=100, blank=True, null=True)
class Meta:
verbose_name = 'Document'
verbose_name_plural = 'Documents'
def __str__(self):
return self.name or self.file.name
Creating an Upload Route
routes.py:
from bazis.contrib.uploadable.routes import FileUploadRouteSet
from bazis.core.schemas.fields import SchemaField, SchemaFields
from django.apps import apps
class DocumentRouteSet(FileUploadRouteSet):
model = apps.get_model('myapp.Document')
# Adding additional fields to schema
fields = {
None: SchemaFields(
include={
'description': None,
'category': None,
},
),
}
router.py:
from bazis.core.routing import BazisRouter
from . import routes
router = BazisRouter(tags=['Documents'])
router.register(routes.DocumentRouteSet.as_router())
Uploading Files via API
Uploading a file with automatic name
POST /api/v1/documents/document/
Content-Type: multipart/form-data
--boundary
Content-Disposition: form-data; name="file"; filename="report.pdf"
Content-Type: application/pdf
[binary data]
--boundary--
Response:
{
"data": {
"type": "myapp.document",
"id": "123e4567-e89b-12d3-a456-426614174000",
"attributes": {
"name": "report.pdf",
"extension": "pdf",
"size": 102400,
"dt_created": "2024-01-15T10:30:00Z"
},
"relationships": {
"file": {
"data": {
"url": "/media/uploads/2024/01/15/report.pdf"
}
}
}
}
}
Uploading a file with custom name
POST /api/v1/documents/document/
Content-Type: multipart/form-data
--boundary
Content-Disposition: form-data; name="file"; filename="original.pdf"
Content-Type: application/pdf
[binary data]
--boundary
Content-Disposition: form-data; name="name"
Monthly Report January 2024.pdf
--boundary--
Response:
{
"data": {
"type": "myapp.document",
"id": "123e4567-e89b-12d3-a456-426614174000",
"attributes": {
"name": "Monthly Report January 2024.pdf",
"extension": "pdf",
"size": 102400
}
}
}
Uploading with additional fields
POST /api/v1/documents/document/
Content-Type: multipart/form-data
--boundary
Content-Disposition: form-data; name="file"; filename="invoice.pdf"
[binary data]
--boundary
Content-Disposition: form-data; name="name"
Invoice #12345
--boundary
Content-Disposition: form-data; name="description"
Invoice for January 2024 services
--boundary
Content-Disposition: form-data; name="category"
Finance
--boundary--
Storage Configuration
Local Storage (default)
# settings.py
MEDIA_ROOT = '/var/www/media/'
MEDIA_URL = '/media/'
# FileSystemStorage is used by default
BAZIS_STORAGE_FILE_UPLOAD = None
Amazon S3
# Install dependencies
uv add django-storages boto3
# settings.py
BAZIS_STORAGE_FILE_UPLOAD = 'storages.backends.s3boto3.S3Boto3Storage'
# S3 configuration
AWS_ACCESS_KEY_ID = 'your-access-key'
AWS_SECRET_ACCESS_KEY = 'your-secret-key'
AWS_STORAGE_BUCKET_NAME = 'your-bucket-name'
AWS_S3_REGION_NAME = 'us-east-1'
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_DEFAULT_ACL = 'public-read' # or None with signed URLs (AWS_QUERYSTRING_AUTH) for private files
MinIO (S3-compatible storage)
# settings.py
BAZIS_STORAGE_FILE_UPLOAD = 'storages.backends.s3boto3.S3Boto3Storage'
AWS_ACCESS_KEY_ID = 'minio-access-key'
AWS_SECRET_ACCESS_KEY = 'minio-secret-key'
AWS_STORAGE_BUCKET_NAME = 'uploads'
AWS_S3_ENDPOINT_URL = 'http://minio:9000'
AWS_S3_USE_SSL = False
AWS_DEFAULT_ACL = 'public-read'
Google Cloud Storage
# Install dependencies
uv add django-storages google-cloud-storage
# settings.py
BAZIS_STORAGE_FILE_UPLOAD = 'storages.backends.gcloud.GoogleCloudStorage'
GS_BUCKET_NAME = 'your-bucket-name'
GS_PROJECT_ID = 'your-project-id'
GS_CREDENTIALS = service_account.Credentials.from_service_account_file(
'path/to/service-account.json'
)
Custom Storage
You can create your own storage by inheriting from Django Storage:
# myapp/storage.py
from django.core.files.storage import FileSystemStorage
class CustomStorage(FileSystemStorage):
def __init__(self):
super().__init__(
location='/custom/path/',
base_url='/custom/'
)
def _save(self, name, content):
# Custom save logic
return super()._save(name, content)
# settings.py
BAZIS_STORAGE_FILE_UPLOAD = 'myapp.storage.CustomStorage'
Examples
Complete document application example
models.py:
from bazis.contrib.uploadable.models_abstract import FileUploadAbstract
from bazis.core.models_abstract import DtMixin, UuidMixin, JsonApiMixin
from django.db import models
class DocumentCategory(models.TextChoices):
INVOICE = 'invoice', 'Invoice'
CONTRACT = 'contract', 'Contract'
REPORT = 'report', 'Report'
OTHER = 'other', 'Other'
class Document(FileUploadAbstract, DtMixin, UuidMixin):
"""Document with category and description"""
description = models.TextField('Description', blank=True, null=True)
category = models.CharField(
'Category',
max_length=20,
choices=DocumentCategory.choices,
default=DocumentCategory.OTHER
)
class Meta:
verbose_name = 'Document'
verbose_name_plural = 'Documents'
def __str__(self):
return f'{self.get_category_display()}: {self.name}'
routes.py:
from bazis.contrib.uploadable.routes import FileUploadRouteSet
from bazis.core.schemas.fields import SchemaFields
from django.apps import apps
class DocumentRouteSet(FileUploadRouteSet):
model = apps.get_model('myapp.Document')
fields = {
None: SchemaFields(
include={
'description': None,
'category': None,
},
),
}
router.py:
from bazis.core.routing import BazisRouter
from . import routes
router = BazisRouter(tags=['Documents'])
router.register(routes.DocumentRouteSet.as_router())
admin.py:
from django.contrib import admin
from bazis.core.admin_abstract import DtAdminMixin
from .models import Document
@admin.register(Document)
class DocumentAdmin(DtAdminMixin, admin.ModelAdmin):
list_display = ('id', 'name', 'category', 'extension', 'size_display', 'dt_created')
list_filter = ('category', 'extension')
search_fields = ('name', 'description')
readonly_fields = ('extension', 'size', 'dt_created', 'dt_updated')
def size_display(self, obj):
"""Human-readable file size"""
size = obj.size
if size < 1024:
return f'{size} B'
elif size < 1024 * 1024:
return f'{size / 1024:.2f} KB'
else:
return f'{size / (1024 * 1024):.2f} MB'
size_display.short_description = 'Size'
Client application usage example
JavaScript (file upload):
async function uploadDocument(file, category, description) {
const formData = new FormData();
formData.append('file', file);
formData.append('category', category);
if (description) {
formData.append('description', description);
}
const response = await fetch('/api/v1/documents/document/', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
if (!response.ok) {
throw new Error('Upload failed');
}
return await response.json();
}
// Usage
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
try {
const result = await uploadDocument(file, 'invoice', 'Monthly invoice');
console.log('Uploaded:', result.data.id);
console.log('File URL:', result.data.attributes.file);
} catch (error) {
console.error('Error:', error);
}
Python (file upload via requests):
import requests
def upload_document(file_path, category, description=None):
url = 'http://api.example.com/api/v1/documents/document/'
with open(file_path, 'rb') as file:
files = {'file': file}
data = {
'category': category,
}
if description:
data['description'] = description
headers = {
'Authorization': f'Bearer {token}'
}
response = requests.post(url, files=files, data=data, headers=headers)
response.raise_for_status()
return response.json()
# Usage
result = upload_document(
'/path/to/invoice.pdf',
category='invoice',
description='Monthly invoice for January'
)
print(f"Uploaded: {result['data']['id']}")
print(f"Size: {result['data']['attributes']['size']} bytes")
Getting list of files
GET /api/v1/documents/document/
Authorization: Bearer <token>
Response:
{
"data": [
{
"type": "myapp.document",
"id": "123e4567-e89b-12d3-a456-426614174000",
"attributes": {
"name": "invoice.pdf",
"extension": "pdf",
"size": 102400,
"category": "invoice",
"description": "Monthly invoice",
"dt_created": "2024-01-15T10:30:00Z"
}
},
{
"type": "myapp.document",
"id": "987e6543-e21b-32d1-b654-426614174001",
"attributes": {
"name": "report.docx",
"extension": "docx",
"size": 256000,
"category": "report",
"dt_created": "2024-01-16T14:20:00Z"
}
}
],
"meta": {
"count": 2
}
}
Filtering by extension
GET /api/v1/documents/document/?filter[extension]=pdf
Filtering by category
GET /api/v1/documents/document/?filter[category]=invoice
License
Apache License 2.0
See LICENSE file for details.
Links
- Bazis Documentation — main repository
- Bazis Uploadable Repository — package repository
- Issue Tracker — report bugs or request features
- Django Storages — storage backend documentation
Support
If you have questions or issues:
- Check the Bazis documentation
- Search through existing issues
- Create a new issue with detailed information
Made with ❤️ by Bazis team
Metadata
Release files for bazis-uploadable 2.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| bazis_uploadable-2.6.0.tar.gz | 93.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bazis_uploadable-2.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 122.6 kB
Release files / bazis_uploadable-2.6.0.tar.gz
| Download URL | bazis_uploadable-2.6.0.tar.gz |
|---|---|
| Size | 93.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
db896038dd782bf2552eb0a7471b9f442615d96dff631d8670ecb3fb227d22a9
|
|
BLAKE2b-256 checksum How to use checksums |
93419ad4a3dc18cac7de1406950011fd3d44a460997955ac6229534ef0109b23
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / bazis_uploadable-2.6.0-py3-none-any.whl
| Download URL | bazis_uploadable-2.6.0-py3-none-any.whl |
|---|---|
| Size | 29.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c29e040f79b26ff1348244e15a44a9eb94b992be8dd0871b6bbdea49930a3317
|
|
BLAKE2b-256 checksum How to use checksums |
2d523bd522e20e6a214590cc4df34d7d6432c9731045beac1d8c8662dc6cead8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|