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 |
|---|---|---|
|
|
Original upload filename |
|
|
Raw file content |
|
|
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.jpgReturns 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:Trueallowed_types(list): Whitelist of allowed MIME types. IfNone, accepts all validated types (⚠️ warning logged)secure_filename(bool): Rename file with UUID for security. Default:Trueprivate(bool): Restrict permissions to owner-only (local0o600instead of0o644, 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
# A .jpg renamed to .txt will still be detected as image/jpegExtension Matching: File extension must match the detected MIME type
# A PNG file with .jpg extension will be rejectedSecure Filenames: Files are renamed with UUID by default
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# ⚠️ 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_typesfor securityUse the most restrictive whitelist possible
Keep
validate=True(default)Use
secure_filename=Truefor 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.Filefields) continues to store the raw filename, maintaining storage independence.When retrieving the field, the
FileRefstring automatically resolves to the S3 cloud URL.