Skip to main content

Bazis Uploadable

PyPI version Python Versions License

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

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

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 file
  • name — filename (optional, automatically filled from file name)
  • extension — file extension (optional, automatically extracted)
  • author, author_updated — the user who uploaded the file (AuthorMixin of bazis-author)
  • uploaded_anonymously — the time a guest uploaded it (None for the files of users and of the server; a model of your own needs makemigrations with 2.8)

Properties:

  • size — file size in bytes (cached_property)

Features:

  • Automatically extracts filename and extension on save
  • Uses get_file_path to generate storage path
  • Supports custom storage backends via settings
  • Returns 0 for size when 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 size and extension fields in schema
  • Supports optional name field for renaming files
  • Returns data in JSON:API format
  • Requires a user; the uploader is the author of the file (guests upload only with anonymous uploads on, see below)

Endpoints:

  • POST / — upload a new file
  • GET /, GET /{id}/ — the files of the user and the visible ones (others are 404)
  • GET /{id}/download/ — the content of such a file, with the access check

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, retrieve and download show a user his own files and the files visible to him through the objects that reference them. Files are not changed or deleted through the API.

A route set of a model that references files declares the fields whose files its readers read; whoever sees an object (its restrict_queryset) reads, downloads and includes them, and several route sets add up:

class CertificateRouteSet(PermitRouteBase):
    model = apps.get_model('training.Certificate')
    files_visible = ('file',)

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 links only his own files (Bazis 2.16): relationships.<field> = {"id": <file id>} with a file of another user fails with 422 ERR_RELATION_LINK if he sees it, else with 403 ERR_RELATION_ACCESS, and ?include=<field> leaves out the files he does not see (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). A file the server generates is stored with FileUpload.objects.create_for(author, name, content); see bazis/contrib/uploadable/AGENTS.md (also for a recipe of a PDF with reproducible fonts).

Anonymous Uploads

Off by default. BAZIS_FILE_UPLOAD_ANONYMOUS = True lets guests (no token) upload files for a public form, only with BAZIS_FILE_UPLOAD_MAX_SIZE set (without it guests upload nothing and bazis_doctor reports uploadable.E002):

Setting Default
BAZIS_FILE_UPLOAD_ANONYMOUS False read when the routes are imported
BAZIS_FILE_UPLOAD_ANONYMOUS_TYPES ['image/jpeg', 'image/png', 'image/webp'] recognized by the content, not the name or Content-Type (422 ERR_FILE_TYPE)
BAZIS_FILE_UPLOAD_ANONYMOUS_RATE '60/hour' per IP address (429); users are not limited
BAZIS_FILE_UPLOAD_ANONYMOUS_TTL 24 hours an upload can be attached

A guest reads back a signed token as the id, name, extension and size (no URL) and reads no file afterwards. A public action takes the ids in its body as list[UploadedFileId] (bazis.contrib.uploadable.schemas, a string, JSON schema x-relation: uploadable.file_upload); for a guest it attaches FileUpload.objects.anonymous(tokens) (valid tokens of files without an author, not expired; a bare id is never taken), for a user filter(pk__in=ids, author=user), and the staff read them through files_visible. A name with a control character is 422 ERR_FILE_NAME. Schedule python manage.py uploadable_clean_anonymous: it deletes the expired anonymous uploads nothing references, with their files. Limit the request body on the reverse proxy, and behind it let uvicorn trust the proxy (--proxy-headers --forwarded-allow-ips) so the rate counts the clients. A frontend contract exports anonymous_uploads() and the types (bazis-front 0.9+: capabilities.uploadable.anonymous, anonymous_types). The recipe and the risks are in the section "Anonymous uploads" of 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: keep private files out of the public media and read them through /{id}/download/, or use signed URLs.

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'
# private objects: the clients read them through /{id}/download/ or signed URLs;
# 'public-read' only for a bucket of public files
AWS_DEFAULT_ACL = None
AWS_QUERYSTRING_AUTH = True

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 = None  # private objects, as for S3
AWS_QUERYSTRING_AUTH = True

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.

Support

If you have questions or issues:


Made with ❤️ by Bazis team

Metadata

Release files for bazis-uploadable 2.8.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for bazis-uploadable 2.8.0
File Size Uploaded Provenance Actions
bazis_uploadable-2.8.0.tar.gz 123.0 kB Without provenance Details for bazis_uploadable-2.8.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for bazis-uploadable 2.8.0
File Interpreter ABI Platform Actions
bazis_uploadable-2.8.0-py3-none-any.whl Python 3 none any Details for bazis_uploadable-2.8.0-py3-none-any.whl

Total release size: 172.8 kB

Release files / bazis_uploadable-2.8.0.tar.gz

Download URL bazis_uploadable-2.8.0.tar.gz
Size 123.0 kB
Tags Source
SHA-256 checksum
How to use checksums
14e656af4ec8834cc7ba3239762b5ecd75151a8a6136251f3711250797597ba4
BLAKE2b-256 checksum
How to use checksums
6ac932d0f125177356dd3b21ec37cb784fea4625cb98acf354d7a35e938d0034
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","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.8.0-py3-none-any.whl

Download URL bazis_uploadable-2.8.0-py3-none-any.whl
Size 49.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b5a7d7c4449db800ed2a7aa7605b49500d2fdd4b90b9d224d4d3f232675940b5
BLAKE2b-256 checksum
How to use checksums
21af593a0f95202238e714697bd7b5a530175cc98afadea838244c77972878ad
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.4 {"installer":{"name":"uv","version":"0.13.4","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for bazis-uploadable 2.8.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page