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

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.7.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.7.0
File Size Uploaded Provenance Actions
bazis_uploadable-2.7.0.tar.gz 105.5 kB Without provenance Details for bazis_uploadable-2.7.0.tar.gz

Built distribution (wheel)

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

Total release size: 142.8 kB

Release files / bazis_uploadable-2.7.0.tar.gz

Download URL bazis_uploadable-2.7.0.tar.gz
Size 105.5 kB
Tags Source
SHA-256 checksum
How to use checksums
8e6ac2b0e8bf0dad7167d6592d8f828b7c59a2608786e63ae2c43dfc05106a28
BLAKE2b-256 checksum
How to use checksums
ecb047ab85bc346e2190af3c3c71d3f3d77337e6cf7d9b241bee62faea8f2d30
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","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.7.0-py3-none-any.whl

Download URL bazis_uploadable-2.7.0-py3-none-any.whl
Size 37.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9250eca5646f9d9ad3c6f2f7b553d812bacdfc1cc5aa98ac01663b80758833f4
BLAKE2b-256 checksum
How to use checksums
529ace9332d314f37ff908d352f8c68a4d5a2a65e480ffe1870dabd2dfbb6cda
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","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}

Security for bazis-uploadable 2.7.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