File Storage
The openviper.storage package provides a pluggable file storage API
for handling uploaded files. The default
FileSystemStorage persists files under
MEDIA_ROOT and builds public URLs from MEDIA_URL.
Overview
All storage operations are coroutine-based and run file I/O in thread
pools via aiofiles. The default_storage singleton is configured
from settings and used automatically by
FileField and
ImageField.
Key Classes
- class openviper.storage.Storage
Protocol defining the interface for all storage backends.
- CHUNK_SIZE
Size of chunks for streaming I/O (class-level
int).
- save(name, content) Awaitable[str]
Persist content (bytes, file-like, or async iterator) under name. Returns the final relative path (may differ from name to avoid collisions).
- delete(name) Awaitable[None]
Delete the file at name.
- exists(name) Awaitable[bool]
Return
Trueif name exists in the storage.
- url(name) str
Return the public URL for name.
- size(name) Awaitable[int]
Return the size in bytes of name.
- read(name) Awaitable[bytes]
Return the full file content. Raises
ValueErrorwhen the file exceedsMAX_READ_SIZE.
- class openviper.storage.FileSystemStorage(location=None, base_url=None, chunk_size=1048576)
Concrete storage that persists files to the local filesystem.
location- directory for files (defaults tosettings.MEDIA_ROOT).base_url- URL prefix for generated URLs (defaults tosettings.MEDIA_URL).chunk_size- chunk size for streaming uploads (default: 1 MiB).
File names are sanitised to remove unsafe characters, and duplicate names are disambiguated by appending a UUID suffix.
- save(name, content) Awaitable[str]
Persist content using an atomic write pattern (temp file + rename). Re-verifies path containment after temp file creation to mitigate TOCTOU.
- delete(name) Awaitable[None]
Delete the file at name. No error if it does not exist.
- exists(name) Awaitable[bool]
Return
Trueif name exists in the storage.
- url(name) str
Return the public URL with percent-encoded path segments.
- size(name) Awaitable[int]
Return the size in bytes. Raises
FileNotFoundErrorif the file does not exist.
- read(name) Awaitable[bytes]
Return the full file content. Raises
ValueErrorwhen the file exceedsMAX_READ_SIZE(100 MiB).
Utilities
- openviper.storage.generate_unique_name(name) str
Generate a collision-resistant file name by appending a full UUID hex suffix. Used internally by
FileSystemStorage.save()when the target name already exists.
Module Constants
Name |
Description |
|---|---|
|
Compiled regex matching characters disallowed in file names. |
|
Compiled regex matching leading-dot (hidden) file names. |
|
Maximum length of a single path component (255). |
|
Maximum file size for |
Type Aliases
Name |
Type |
|---|---|
|
|
Default Storage Proxy
- class openviper.storage.DefaultStorage
Thread-safe lazy proxy that creates a
FileSystemStorageon first access.- configure(storage) None
Programmatically override the default storage backend.
Delegates all
Storagemethods (save,delete,exists,url,size,read,listdir) to the underlying instance.
- openviper.storage.default_storage
Module-level
DefaultStoragesingleton.
Example Usage
Using default_storage Directly
from openviper.storage import default_storage
from openviper.http.request import Request
from openviper.http.response import JSONResponse
from openviper.routing.router import Router
router = Router()
@router.post("/upload")
async def upload_file(request: Request) -> JSONResponse:
files = await request.files()
file = files.get("file")
if file is None:
return JSONResponse({"error": "No file provided"}, status_code=400)
content = await file.read()
saved_path = await default_storage.save(f"uploads/{file.filename}", content)
url = default_storage.url(saved_path)
return JSONResponse({"path": saved_path, "url": url})
FileField on a Model
When a model has a FileField, the framework
automatically uses default_storage to persist the file on save():
from openviper.db.models import Model
from openviper.db import fields
class Document(Model):
class Meta:
table_name = "documents"
title = fields.CharField(max_length=255)
file = fields.FileField(upload_to="documents/")
# In a view:
@router.post("/documents")
async def upload_doc(request: Request) -> JSONResponse:
form = await request.form()
uploaded = (await request.files()).get("file")
doc = Document(title=form["title"], file=uploaded)
await doc.save()
return JSONResponse({"url": default_storage.url(doc.file)}, status_code=201)
Custom Storage Backend
Implement the Storage protocol to integrate any cloud or
third-party storage:
from openviper.storage.base import Storage
class S3Storage:
def __init__(self, bucket: str) -> None:
self.bucket = bucket
async def save(self, name: str, content: bytes) -> str:
# Upload to S3 ...
return name
async def delete(self, name: str) -> None:
# Delete from S3 ...
pass
def url(self, name: str) -> str:
return f"https://{self.bucket}.s3.amazonaws.com/{name}"
async def exists(self, name: str) -> bool:
# Check S3 ...
return False
async def size(self, name: str) -> int:
return 0
async def read(self, name: str) -> bytes:
# Fetch from S3 ...
return b""
async def listdir(self, path: str = "") -> list[str]:
return []
Configuration
@dataclasses.dataclass(frozen=True)
class MySettings(Settings):
MEDIA_ROOT: str = "media"
MEDIA_URL: str = "/media/"
DEFAULT_FILE_STORAGE: str = "myproject.storage.S3Storage"
API Reference
OpenViper file storage backends.
- class openviper.storage.FileSystemStorage(location=None, base_url=None, chunk_size=1048576)[source]
Bases:
objectStore files on the local filesystem with async I/O and streaming.
- Parameters:
- validate_name(name)[source]
Validate and sanitise name against path traversal.
Rejects null bytes.
Replaces hidden filenames (leading dot).
Normalises separators.
Removes
..components (path traversal guard).Truncates each component to
MAX_COMPONENT_LEN.
- Raises:
ValueError – if name is empty, contains null bytes, or resolves to an empty path after sanitisation.
- Parameters:
name (str)
- Return type:
- full_path(name)[source]
Return absolute path, guaranteed to be inside
location.Performs symlink detection and path containment verification to prevent directory traversal attacks. Containment is checked before symlink inspection so that escape attempts are rejected regardless of symlink state.
- Raises:
ValueError – if the resolved path escapes the storage root, or if the path involves symlinks.
- Parameters:
name (str)
- Return type:
- async mkdir_async(path)[source]
Create path directory (and parents) asynchronously.
- Parameters:
path (Path)
- Return type:
None
- resolved_path(name)[source]
Validate name and return its absolute path inside
location.Combines
validate_name()andfull_path()into a single call so callers do not repeat the two-step dance.
- async save(name, content)[source]
Save file with async I/O and chunked uploads.
Uses an atomic write pattern (temp file + rename) to prevent partial writes. Re-verifies path containment after temp file creation to mitigate TOCTOU.
- Parameters:
name (str)
content (StorageContent)
- Return type:
- async delete(name)[source]
Delete file asynchronously. No error if it does not exist.
- Parameters:
name (str)
- Return type:
None
- async read(name)[source]
Read and return the full content of the file at name.
- Raises:
ValueError – if the file exceeds
MAX_READ_SIZEbytes.- Parameters:
name (str)
- Return type:
- class openviper.storage.Storage(*args, **kwargs)[source]
Bases:
ProtocolProtocol defining the interface for all storage backends.
- async save(name, content)[source]
- Parameters:
name (str)
content (StorageContent)
- Return type:
- openviper.storage.generate_unique_name(name)[source]
Generate a collision-resistant name via UUID hex suffix.
File storage backends for OpenViper.
Pluggable storage API for handling uploaded files.
FileSystemStorage persists to MEDIA_ROOT
and serves at MEDIA_URL.
- class openviper.storage.base.Storage(*args, **kwargs)[source]
Bases:
ProtocolProtocol defining the interface for all storage backends.
- async save(name, content)[source]
- Parameters:
name (str)
content (StorageContent)
- Return type:
- openviper.storage.base.generate_unique_name(name)[source]
Generate a collision-resistant name via UUID hex suffix.
- class openviper.storage.base.FileSystemStorage(location=None, base_url=None, chunk_size=1048576)[source]
Bases:
objectStore files on the local filesystem with async I/O and streaming.
- Parameters:
- validate_name(name)[source]
Validate and sanitise name against path traversal.
Rejects null bytes.
Replaces hidden filenames (leading dot).
Normalises separators.
Removes
..components (path traversal guard).Truncates each component to
MAX_COMPONENT_LEN.
- Raises:
ValueError – if name is empty, contains null bytes, or resolves to an empty path after sanitisation.
- Parameters:
name (str)
- Return type:
- full_path(name)[source]
Return absolute path, guaranteed to be inside
location.Performs symlink detection and path containment verification to prevent directory traversal attacks. Containment is checked before symlink inspection so that escape attempts are rejected regardless of symlink state.
- Raises:
ValueError – if the resolved path escapes the storage root, or if the path involves symlinks.
- Parameters:
name (str)
- Return type:
- async mkdir_async(path)[source]
Create path directory (and parents) asynchronously.
- Parameters:
path (Path)
- Return type:
None
- resolved_path(name)[source]
Validate name and return its absolute path inside
location.Combines
validate_name()andfull_path()into a single call so callers do not repeat the two-step dance.
- async save(name, content)[source]
Save file with async I/O and chunked uploads.
Uses an atomic write pattern (temp file + rename) to prevent partial writes. Re-verifies path containment after temp file creation to mitigate TOCTOU.
- Parameters:
name (str)
content (StorageContent)
- Return type:
- async delete(name)[source]
Delete file asynchronously. No error if it does not exist.
- Parameters:
name (str)
- Return type:
None
- async read(name)[source]
Read and return the full content of the file at name.
- Raises:
ValueError – if the file exceeds
MAX_READ_SIZEbytes.- Parameters:
name (str)
- Return type:
- class openviper.storage.base.DefaultStorage[source]
Bases:
objectThread-safe lazy proxy for the default storage backend.
- configure(storage)[source]
Programmatically override the default storage backend.
- Parameters:
storage (FileSystemStorage)
- Return type:
None
- async save(name, content)[source]
- Parameters:
name (str)
content (StorageContent)
- Return type: