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.2

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.2
File Size Uploaded
bazis_uploadable-2.6.2.tar.gz 94.2 kB Details

Built distribution (wheel)

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

Total release size: 123.7 kB

Release files / bazis_uploadable-2.6.2.tar.gz

Download URL bazis_uploadable-2.6.2.tar.gz
Size 94.2 kB
Tags Source
SHA-256 checksum
How to use checksums
9201a97fe7eac29649c18bd7acc82b411a644bfb0b8abd9d461624aec5b35841
BLAKE2b-256 checksum
How to use checksums
2ffdc523ab8da36283c73398d618b1cab8cefae4bcf78ed038c674cf8825062d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.24 {"installer":{"name":"uv","version":"0.12.24","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.2-py3-none-any.whl

Download URL bazis_uploadable-2.6.2-py3-none-any.whl
Size 29.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
192ee9a29a5396c24f40994f7d3f3e3eef4aaf595524fcc551a90a6a7c7f713f
BLAKE2b-256 checksum
How to use checksums
550ee6d7de2b41fc6df4adca326449bbf060cc21e6bfd68ef4a91bd2a7d15832
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.24 {"installer":{"name":"uv","version":"0.12.24","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

2.6.3

2 release files

This release

2.6.2 This release

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