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)

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

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.

Support

If you have questions or issues:


Made with ❤️ by Bazis team

Metadata

Release files for bazis-uploadable 2.6.3

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.6.3
File Size Uploaded
bazis_uploadable-2.6.3.tar.gz 94.5 kB Details

Built distribution (wheel)

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

Total release size: 124.2 kB

Release files / bazis_uploadable-2.6.3.tar.gz

Download URL bazis_uploadable-2.6.3.tar.gz
Size 94.5 kB
Tags Source
SHA-256 checksum
How to use checksums
6375dddf9cc693bed4a4cd08f53b4a7d803c2a2110dbeded72e59487ac120dee
BLAKE2b-256 checksum
How to use checksums
5fd48e904c999ad46153e823785564dfb61f0a605937d0b411be5a72086c139b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","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.3-py3-none-any.whl

Download URL bazis_uploadable-2.6.3-py3-none-any.whl
Size 29.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9adfc2c5cc3c190ae174649e75c33f7a39e5ff6860b4eaeedee2864e31c095e0
BLAKE2b-256 checksum
How to use checksums
5ba03a35d51993c45f16616cf5dd9675e92a7e4823355812a5ee762614c9b398
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","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 history Release notifications | RSS feed

This release

2.6.3 This release

2 release files

2.6.2

2 release files

2.6.1

2 release files

2.6.0

2 release files

2.5.0

2 release files

2.4.0

2 release files

2.3.0

2 release files

2.2.0

2 release files

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