File Storage#
Keywords: file upload, upload directories, serving files, secure downloads, storage backend, multipart form
UploadedFile#
Multipart file uploads are parsed into UploadedFile objects available on request.files.
from asok import Request
def render(request: Request):
photo = request.files.get("photo")
if photo:
print(photo.filename) # "avatar.jpg"
print(photo.size) # 102400
saved = photo.save("avatar.jpg")
# Returns actual path (may differ if conflict)
Properties#
| Property | Type | Description |
|---|---|---|
filename | str | Original upload filename |
content | bytes | Raw file content |
size | int | File size in bytes |
save(destination)#
Saves the file to disk. By default, relative paths are resolved relative to src/partials/uploads/.
photo.save("avatar.jpg") # saves to src/partials/uploads/avatar.jpg
photo.save("imgs/avatar.png") # saves to src/partials/uploads/imgs/avatar.png
Features: - Auto-creates parent directories - Handles name conflicts: photo.jpg → photo_1.jpg → photo_2.jpg - Returns the actual saved absolute path
MIME Type Validation ⭐ NEW in v0.1.6#
Asok provides automatic MIME type validation using magic bytes detection to prevent malicious file uploads. This security feature validates files based on their actual content, not just their extension.
Basic Usage#
from asok import Request
def render(request: Request):
photo = request.files.get("photo")
if photo:
try:
# Validate and save - only allow images
photo.save("uploads/", allowed_types=['image/jpeg', 'image/png'])
except ValueError as e:
return f"Error: {e}"
Validation Parameters#
The save() method accepts these validation parameters:
photo.save(
destination="uploads/",
validate=True, # Enable MIME validation (default: True)
allowed_types=['image/jpeg', 'image/png'], # Whitelist of MIME types
secure_filename=True, # Rename with UUID (default: True)
private=True # Restrict permissions to owner-only (default: False)
)
Parameters: - validate (bool): Enable/disable validation. Default: True - allowed_types (list): Whitelist of allowed MIME types. If None, accepts all validated types (⚠️ warning logged) - secure_filename (bool): Rename file with UUID for security. Default: True - private (bool): Restrict permissions to owner-only (local 0o600 instead of 0o644, or S3 "private" ACL). Default: False
Supported File Types (50+ formats)#
Asok validates files using magic bytes (file signatures) for accuracy:
🖼️ Images (11 formats)#
allowed_types = [
'image/jpeg', # .jpg, .jpeg
'image/png', # .png
'image/gif', # .gif
'image/webp', # .webp (modern format)
'image/bmp', # .bmp
'image/tiff', # .tif, .tiff
'image/x-icon', # .ico
'image/svg+xml', # .svg (vector)
]
🎵 Audio (8 formats)#
allowed_types = [
'audio/mpeg', # .mp3
'audio/wav', # .wav
'audio/flac', # .flac (lossless)
'audio/ogg', # .ogg, .oga
'audio/aac', # .aac
'audio/mp4', # .m4a
]
🎬 Video (6 formats)#
allowed_types = [
'video/mp4', # .mp4 (H.264)
'video/webm', # .webm (VP8/VP9)
'video/x-matroska',# .mkv
'video/avi', # .avi
'video/quicktime', # .mov
'video/3gpp', # .3gp (mobile)
]
📄 Documents & Archives#
allowed_types = [
'application/pdf', # .pdf
'application/zip', # .zip, .docx, .xlsx, .pptx
'application/msword', # .doc, .xls, .ppt (legacy)
'text/rtf', # .rtf
'application/gzip', # .gz
'application/x-bzip2', # .bz2
'application/x-rar-compressed', # .rar
'application/x-7z-compressed', # .7z
]
Advanced Validation#
Standalone Validation#
You can validate files without saving:
photo = request.files.get("photo")
try:
photo.validate_mime_type(allowed_types=['image/jpeg', 'image/png'])
print("✅ File is valid")
except ValueError as e:
print(f"❌ Invalid file: {e}")
Multiple File Types#
# Accept images and PDFs
allowed = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf']
document.save("docs/", allowed_types=allowed)
Media Gallery#
# Accept all images, audio, and video
allowed = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
'audio/mpeg', 'audio/wav',
'video/mp4', 'video/webm'
]
media.save("gallery/", allowed_types=allowed)
Security Features#
-
Magic Bytes Detection: Files are validated by their actual content, not extension
python # A .jpg renamed to .txt will still be detected as image/jpeg -
Extension Matching: File extension must match the detected MIME type
python # A PNG file with .jpg extension will be rejected -
Secure Filenames: Files are renamed with UUID by default
python photo.save("uploads/") # → uploads/a3f2b9c1-4567-89ab-cdef.jpg -
Restrictive Permissions: Saved files get
0o644(rw-r--r--) permissions by default, but can be locked down to0o600(rw-------, owner only) usingprivate=True(e.g. for sensitive/non-public uploads). -
Security Warnings: Logs warning if
allowed_typesnot specified ```python # ⚠️ Generates security warning photo.save("uploads/") # No allowed_types!
# ✅ Recommended photo.save("uploads/", allowed_types=['image/jpeg']) ```
Error Handling#
from asok import Request
def render(request: Request):
photo = request.files.get("photo")
if not photo:
return "No file uploaded"
try:
path = photo.save(
"avatars/",
allowed_types=['image/jpeg', 'image/png', 'image/webp']
)
return f"✅ File saved: {path}"
except ValueError as e:
# Handle validation errors
if "not allowed" in str(e):
return "Only JPEG, PNG, and WebP images are allowed"
elif "does not match" in str(e):
return "File extension doesn't match file type"
else:
return f"Validation error: {e}"
Best Practices#
✅ DO: - Always specify allowed_types for security - Use the most restrictive whitelist possible - Keep validate=True (default) - Use secure_filename=True for public uploads
# ✅ Secure upload
avatar.save("avatars/", allowed_types=['image/jpeg', 'image/png'])
❌ DON'T: - Don't disable validation (validate=False) - Don't allow all types without restriction - Don't trust file extensions alone
# ❌ Insecure - accepts any file!
file.save("uploads/", validate=False)
# ⚠️ Less secure - accepts any valid type
file.save("uploads/") # allowed_types=None
Performance Notes#
- Validation reads only the first few bytes of each file (magic bytes)
- Minimal performance impact even for large files
- Files are validated before writing to disk
Accessing files#
request.files is a dictionary of UploadedFile objects. You can access them using bracket notation or .get() (safest):
# Safest: returns None if missing
photo = request.files.get("photo")
# Alternative: raises KeyError if missing
photo = request.files["photo"]
# Accessing properties (preferred)
print(photo.filename)
# Dict-style access (backward compatibility)
print(request.files["photo"]["filename"])
Serving Files#
Use request.send_file() to return a file to the browser. Relative paths are automatically resolved relative to src/partials/uploads/.
from asok import Request
def render(request: Request):
# Resolves to src/partials/uploads/report.pdf
return request.send_file("report.pdf")
# Resolves to src/partials/uploads/pdf/cv.pdf
return request.send_file("pdf/cv.pdf")
# Force download with custom name
return request.send_file("data.csv", filename="export.csv")
# Display image in browser (inline)
return request.send_file("header.png", as_attachment=False)
Path Resolution#
All paths passed to request.send_file() are resolved relative to src/partials/uploads/ (leading and root slash prefixes are stripped for security).
For security,
request.send_file()only allows serving files from within thesrc/partials/uploadsdirectory. Attempts to escape or access files outside this directory will return a403 Forbiddenerror.
Configuration#
1. Upload Size Limits#
You can limit the maximum upload size globally in your Asok app configuration:
# wsgi.py
app = Asok()
app.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 # 16 MB limit
Default is 10 MB. If a request exceeds this limit, Asok returns a 413 Payload Too Large error.
2. Storage Backends (S3 Cloud Storage)#
Asok supports abstract storage backends. By default, it uses local storage. You can switch to s3 for storing uploads in Amazon S3 or S3-compatible endpoints (like MinIO or DigitalOcean Spaces).
To use S3, install the optional extra:
pip install "asok[s3]"
Configure your .env file:
# Enable S3 backend (default: local)
ASOK_STORAGE_BACKEND=s3
# S3 Credentials
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
# S3 Bucket configuration
ASOK_S3_BUCKET=your-bucket-name
ASOK_S3_REGION=us-east-1
# Optional: Endpoint for S3-compatible storage (e.g. MinIO)
# ASOK_S3_ENDPOINT=http://localhost:9000
# Optional: Custom CDN Domain for URLs
# ASOK_S3_CUSTOM_DOMAIN=cdn.myapp.com
When s3 is enabled: * photo.save() automatically uploads to S3 and returns the public S3 URL of the file. * Your database (Field.File fields) continues to store the raw filename, maintaining storage independence. * When retrieving the field, the FileRef string automatically resolves to the S3 cloud URL.
Was this page helpful?