Direct Uploads
Direct uploads send files through your server, giving you full control to validate, transform, or scan files before storing them. This is the right choice for many applications— it’s simple to set up and covers common use cases like profile photos, documents, and small media files.
Quick Start
Here’s a minimal working upload in three parts: configuration, template, and event handling.
1. Configure the Upload
In your LiveView’s mount(), call allow_upload() to define what files you’ll accept:
from pyview import LiveView, LiveViewSocketfrom pyview.uploads import UploadConfig, UploadConstraintsfrom dataclasses import dataclass
@dataclassclass UploadContext: upload_config: UploadConfig uploaded_files: list = None
def __post_init__(self): if self.uploaded_files is None: self.uploaded_files = []
class FileUploadView(LiveView[UploadContext]): async def mount(self, socket: LiveViewSocket[UploadContext], session): config = socket.allow_upload( "documents", constraints=UploadConstraints( max_file_size=5 * 1024 * 1024, # 5MB max_files=3, accept=[".pdf", ".doc", ".docx"] ) ) socket.context = UploadContext(upload_config=config)2. Add the Template
The live_file_input filter creates the file input, and phx-drop-target enables drag-and-drop:
<form phx-submit="save" phx-change="validate"> <div phx-drop-target="{{upload_config.ref}}" class="upload-zone"> <p>Drop files here or click to select</p> {{ upload_config | live_file_input }} </div>
<!-- Show files waiting to upload --> {% for entry in upload_config.entries %} <div class="upload-entry"> <span>{{ entry.name }}</span> <div class="progress" style="width: {{ entry.progress }}%"></div> <button type="button" phx-click="cancel" phx-value-ref="{{ entry.ref }}">×</button> </div> {% endfor %}
<!-- Show any errors --> {% for error in upload_config.errors %} <div class="error">{{ error.message }}</div> {% endfor %}
<button type="submit">Upload</button></form>3. Handle the Upload
When the form submits, use consume_uploads() to access the uploaded files:
async def handle_event(self, event, payload, socket: LiveViewSocket[UploadContext]): if event == "save": with socket.context.upload_config.consume_uploads() as uploads: for upload in uploads: # upload.file is a temp file, upload.entry has metadata saved_path = await self.save_file(upload.file, upload.entry.name) socket.context.uploaded_files.append({ "name": upload.entry.name, "path": saved_path })
elif event == "cancel": socket.context.upload_config.cancel_entry(payload["ref"])
async def save_file(self, temp_file, filename: str) -> str: import os, shutil, uuid os.makedirs("uploads", exist_ok=True) file_id = str(uuid.uuid4()) ext = os.path.splitext(filename)[1] path = f"uploads/{file_id}{ext}" shutil.copy2(temp_file.name, path) return pathThat’s it! You now have working file uploads with progress tracking and drag-and-drop.
Going Further
Once basic uploads are working, you might want to add auto-upload, image previews, or custom validation. Each section below builds on the basics.
Auto-Upload Mode
By default, files wait until the form is submitted. With auto_upload=True, files start uploading immediately when selected—great for photo galleries or when you want instant feedback:
config = socket.allow_upload( "photos", constraints=UploadConstraints( max_file_size=10 * 1024 * 1024, max_files=10, accept=[".jpg", ".jpeg", ".png"] ), auto_upload=True, progress=self.handle_progress # Called on each progress update)The progress callback fires as uploads progress. Check entry.done to know when a file finishes:
async def handle_progress(self, entry, socket): if entry.done: # Process this file immediately instead of waiting for form submit with socket.context.upload_config.consume_upload_entry(entry.ref) as upload: if upload: path = await self.save_file(upload.file, upload.entry.name) socket.context.uploaded_files.append({"name": entry.name, "path": path})Image Previews
For image uploads, you can show thumbnails before the upload completes using the upload_preview_tag filter:
{% for entry in upload_config.entries %} <div class="preview-item"> {{ entry | upload_preview_tag }} <span>{{ entry.name }}</span> </div>{% endfor %}This generates a client-side preview—no server round-trip needed.
Constraints and Validation
Built-in Constraints
UploadConstraints handles the common cases:
constraints = UploadConstraints( max_file_size=10 * 1024 * 1024, # 10MB per file max_files=5, # Max 5 files at once accept=[".jpg", ".png", ".pdf"], # Allowed extensions chunk_size=64 * 1024 # 64KB chunks (for large files))When constraints are violated, errors appear in upload_config.errors automatically.
Error Handling
Errors can come from constraint violations or your own code. Here’s how to handle both:
async def handle_progress(self, entry, socket): if entry.done: try: with socket.context.upload_config.consume_upload_entry(entry.ref) as upload: if upload: await self.process_file(upload.file, upload.entry) except Exception as e: # Set an error message the user can see socket.context.error_message = f"Failed to process {entry.name}" import logging logging.exception(f"Upload error: {entry.name}")In your template, display errors from both the upload config and your own handling:
{% for error in upload_config.errors %} <div class="error">{{ error.message }}</div>{% endfor %}
{% if error_message %} <div class="error">{{ error_message }}</div>{% endif %}Security Best Practices
PyView handles temp file cleanup automatically, but you should still be careful with user-uploaded files.
Store Files Safely
Never use the original filename directly—generate a unique name:
import os, uuid, shutilfrom pathlib import Path
async def save_securely(temp_file, original_name: str) -> dict: upload_dir = Path("uploads") upload_dir.mkdir(exist_ok=True)
# Generate safe filename file_id = str(uuid.uuid4()) ext = Path(original_name).suffix.lower()
# Validate extension allowed = {'.jpg', '.jpeg', '.png', '.pdf'} if ext not in allowed: raise ValueError(f"Extension {ext} not allowed")
safe_path = upload_dir / f"{file_id}{ext}" shutil.copy2(temp_file.name, safe_path)
return {"id": file_id, "path": str(safe_path), "original_name": original_name}Complete Example
Here’s a focused photo upload with auto-upload and thumbnails:
from datetime import datetimefrom pyview import LiveView, LiveViewSocket, is_connectedfrom pyview.uploads import UploadConfig, UploadConstraintsfrom dataclasses import dataclass, fieldimport os, uuid, shutil, base64from PIL import Imagefrom io import BytesIO
@dataclassclass PhotoContext: upload_config: UploadConfig photos: list = field(default_factory=list) error: str = ""
class PhotoUploadView(LiveView[PhotoContext]): async def mount(self, socket: LiveViewSocket[PhotoContext], session): config = socket.allow_upload( "photos", constraints=UploadConstraints( max_file_size=10 * 1024 * 1024, max_files=10, accept=[".jpg", ".jpeg", ".png"] ), auto_upload=True, progress=self.on_progress ) socket.context = PhotoContext(upload_config=config)
async def on_progress(self, entry, socket): if entry.done and entry.valid: try: with socket.context.upload_config.consume_upload_entry(entry.ref) as upload: if upload: photo = await self.save_photo(upload.file, upload.entry) socket.context.photos.append(photo) socket.context.error = "" except Exception as e: socket.context.error = f"Failed: {entry.name}"
async def save_photo(self, temp_file, entry) -> dict: photo_id = str(uuid.uuid4()) os.makedirs("uploads", exist_ok=True)
ext = os.path.splitext(entry.name)[1].lower() path = f"uploads/{photo_id}{ext}" shutil.copy2(temp_file.name, path)
# Create thumbnail with Image.open(temp_file.name) as img: img.thumbnail((150, 150)) buf = BytesIO() img.save(buf, format='JPEG', quality=80) thumb = base64.b64encode(buf.getvalue()).decode()
return {"id": photo_id, "name": entry.name, "path": path, "thumb": thumb}
async def handle_event(self, event, payload, socket): if event == "cancel": socket.context.upload_config.cancel_entry(payload["ref"])For large files or when you want to reduce server load, see External Uploads.