Skip to content
docs.djust.org

File Uploads

djust provides chunked binary file uploads over WebSocket, with client-side previews, progress tracking, drag-and-drop support, and server-side validation. Inspired by Phoenix LiveView's upload system.

Overview

  • UploadMixin - Server-side mixin with allow_upload() configuration and consume_uploaded_entries() processing
  • Chunked binary transfer - Files are split into 64KB chunks and sent as binary WebSocket frames
  • Template directives - dj-upload, dj-upload-drop, dj-upload-preview, dj-upload-progress
  • Validation - File size limits, extension filtering, MIME type checking, and magic byte verification
  • Progress tracking - Real-time progress updates via djust:upload:progress DOM events

Quick Start

1. Configure uploads in your view

from djust import LiveView
from djust.uploads import UploadMixin
from djust.decorators import event_handler
from django.core.files.storage import default_storage

class ProfileView(UploadMixin, LiveView):
    template_name = 'profile.html'

    def mount(self, request, **kwargs):
        self.avatar_url = ""
        self.allow_upload('avatar',
            accept='.jpg,.png,.webp',
            max_entries=1,
            max_file_size=5_000_000,  # 5MB
        )

    @event_handler()
    def save_avatar(self, **kwargs):
        for entry in self.consume_uploaded_entries('avatar'):
            path = default_storage.save(
                f'avatars/{entry.client_name}', entry.file
            )
            self.avatar_url = default_storage.url(path)

2. Add upload elements to your template

<form dj-submit="save_avatar">
    <!-- File input bound to the 'avatar' upload slot -->
    <input type="file" dj-upload="avatar">

    <!-- Image preview container -->
    <div dj-upload-preview="avatar"></div>

    <!-- Progress bar container -->
    <div dj-upload-progress="avatar"></div>

    <button type="submit">Save</button>
</form>

3. Drag-and-drop zone

<div dj-upload-drop="avatar" class="drop-zone">
    <p>Drag and drop your avatar here</p>
    <input type="file" dj-upload="avatar">
    <div dj-upload-preview="avatar"></div>
</div>

API Reference

UploadMixin Methods

allow_upload(name, accept="", max_entries=1, max_file_size=10_000_000, chunk_size=65536, auto_upload=True)

Configure a named upload slot. Call this in mount().

ParameterTypeDefaultDescription
namestrrequiredUpload slot name, referenced in templates as dj-upload="name".
acceptstr""Comma-separated accepted extensions or MIME types (e.g., ".jpg,.png" or "image/*"). Empty allows all.
max_entriesint1Maximum number of files for this slot. Sets multiple attribute automatically if > 1.
max_file_sizeint10_000_000Maximum file size in bytes (default 10MB).
chunk_sizeint65536Chunk size for transfer (default 64KB).
auto_uploadboolTrueStart upload immediately when files are selected.
# Single image upload
self.allow_upload('avatar', accept='.jpg,.png,.webp',
                  max_entries=1, max_file_size=5_000_000)

# Multiple document upload
self.allow_upload('documents', accept='.pdf,.docx',
                  max_entries=10, max_file_size=20_000_000)

consume_uploaded_entries(name) -> Generator[UploadEntry]

Consume completed upload entries for a named slot. Yields UploadEntry objects and cleans them up after iteration.

for entry in self.consume_uploaded_entries('avatar'):
    # entry.client_name - Original filename
    # entry.client_type - MIME type
    # entry.client_size - File size in bytes
    # entry.data        - Complete file as bytes
    # entry.file        - File as BytesIO object
    # entry.progress    - Upload progress (0-100)
    # entry.complete    - Whether upload is complete
    # entry.error       - Error message or None
    save_file(entry.client_name, entry.file)

get_uploads(name) -> list[UploadEntry]

Get all entries (including in-progress) for an upload slot. Does not consume them.

cancel_upload(name, ref)

Cancel a specific upload by its reference ID.

UploadEntry Properties

PropertyTypeDescription
client_namestrOriginal filename from the client
client_typestrMIME type from the client
client_sizeintExpected file size in bytes
databytesComplete file content as bytes
fileBytesIOFile-like object for use with Django's storage API
progressintUpload progress percentage (0-100)
completeboolWhether the upload is finished
errorstr or NoneError message if validation failed

Template Directives

dj-upload="name"

Bind a file input to an upload slot. The accept and multiple attributes are set automatically from the upload configuration.

<input type="file" dj-upload="avatar">

dj-upload-drop="name"

Create a drag-and-drop zone for file uploads. The upload-dragover CSS class is added during drag operations.

<div dj-upload-drop="photos" class="drop-zone">
    Drop files here or <input type="file" dj-upload="photos">
</div>

<style>
.drop-zone { border: 2px dashed #ccc; padding: 2rem; text-align: center; }
.drop-zone.upload-dragover { border-color: #007bff; background: #f0f8ff; }
</style>

dj-upload-preview="name"

Container for image previews. Automatically populated when image files are selected. Each preview includes:

  • .upload-preview-item wrapper
  • .upload-preview-image (for image files)
  • .upload-preview-name (filename)
  • .upload-preview-size (formatted size)
<div dj-upload-preview="avatar" class="preview-container"></div>

dj-upload-progress="name"

Container for progress bars. Each uploading file gets:

  • .upload-progress-item[data-upload-ref] wrapper
  • .upload-progress-name (filename)
  • .upload-progress-track > .upload-progress-bar[role=progressbar]
  • .upload-progress-text (percentage)
<div dj-upload-progress="documents"></div>

Client-Side Events

EventDetailDescription
djust:upload:progress{ref, progress, status, uploadName}Upload progress update
djust:upload:error{file, error}Client-side validation error
window.addEventListener('djust:upload:progress', (e) => {
    console.log(`${e.detail.uploadName}: ${e.detail.progress}%`);
});

Examples

Image Upload with Preview

class GalleryView(UploadMixin, LiveView):
    template_name = 'gallery.html'

    def mount(self, request, **kwargs):
        self.images = []
        self.allow_upload('photos',
            accept='.jpg,.png,.webp,.gif',
            max_entries=5,
            max_file_size=10_000_000,
        )

    @event_handler()
    def upload_photos(self, **kwargs):
        for entry in self.consume_uploaded_entries('photos'):
            path = default_storage.save(
                f'gallery/{entry.client_name}', entry.file
            )
            self.images.append({
                'url': default_storage.url(path),
                'name': entry.client_name,
            })
<div dj-upload-drop="photos" class="drop-zone">
    <p>Drag photos here or click to browse</p>
    <input type="file" dj-upload="photos">
</div>

<div dj-upload-preview="photos" class="preview-grid"></div>
<div dj-upload-progress="photos"></div>

<button dj-click="upload_photos">Upload All</button>

<div class="gallery">
    {% for img in images %}
    <img src="{{ img.url }}" alt="{{ img.name }}">
    {% endfor %}
</div>

Document Upload with Validation Feedback

class DocumentUploadView(UploadMixin, LiveView):
    template_name = 'upload_docs.html'

    def mount(self, request, **kwargs):
        self.uploaded_files = []
        self.errors = []
        self.allow_upload('docs',
            accept='.pdf,.docx,.txt',
            max_entries=3,
            max_file_size=20_000_000,
        )

    @event_handler()
    def process_uploads(self, **kwargs):
        self.errors = []
        for entry in self.consume_uploaded_entries('docs'):
            if entry.error:
                self.errors.append(f"{entry.client_name}: {entry.error}")
            else:
                path = default_storage.save(
                    f'documents/{entry.client_name}', entry.file
                )
                self.uploaded_files.append({
                    'name': entry.client_name,
                    'size': entry.client_size,
                    'url': default_storage.url(path),
                })

Best Practices

File Size Limits

  • Set max_file_size based on your application's needs. The default is 10MB.
  • Client-side validation rejects oversized files before upload begins.
  • Server-side validation double-checks size after all chunks are received (with 10% tolerance for encoding overhead).

Accepted File Types

  • Use file extensions (.jpg,.png) for simple filtering, or MIME types (image/*) for broader categories.
  • Server-side validation includes magic byte checking for common file types (JPEG, PNG, GIF, WebP, PDF, etc.) to prevent extension spoofing.

Progress Feedback

  • Progress bars are automatically created in dj-upload-progress containers.
  • Listen for the djust:upload:progress event for custom progress UI.
  • The status field in progress events is one of: "uploading", "complete", "error", "cancelled".

Cleanup

  • consume_uploaded_entries() cleans up temp files after iteration. Always iterate fully or call cancel_upload() for files you do not want.
  • Temp files are stored in a session-specific directory and cleaned up on WebSocket disconnect.
  • For large files, consider increasing chunk_size to reduce the number of WebSocket frames.