Moodle File Subsystem Architecture: Requirements, Design, and Operational Safeguards
Moodle’s file subsystem relies on a thin API that delegates to pluggable backends. This guide covers the core requirements, minimal design, trust boundaries, operational checks, failure modes, and when the design should change—complete with examples and a practical audit checklist.
01 Apr 2026, 15:49 UTC

Problem & Takeaway
Moodle must accept user uploads, enforce limits, keep files safe, and let plugins plug into storage without touching core code. The core file API and a thin file_storage abstraction give a minimal, secure, and extensible design. Understanding the trust boundaries and operational checks lets you audit, troubleshoot, and evolve the system.
1. Requirements
- Secure storage of untrusted uploads.
- Enforce per‑context size limits and site‑wide maximum.
- Serve files via a URL that respects capabilities.
- Support versioning and plugin‑driven backends (e.g., AWS S3).
- Maintain compatibility with existing plugins that rely on the file API.
2. Minimal Suitable Design
Moodle’s file system is built around a single public entry point: pluginfile.php (or file.php in older releases). All uploads go through lib/filelib.php, which creates a file_record and hands it to the file_storage class. The storage class resolves the physical path, checks permissions, and streams the file. Key points:
- Thin controller:
pluginfile.phpvalidates the request, loads the record, and delegates tofile_storage::get_file(). - Abstraction layer:
file_storageimplementsget_file(),save_file(), anddelete_file(). Backends are pluggable viafile_storage_{backend}classes. - Path resolution: The file is stored in
$CFG->dataroot/filedirwith a hashed path that hides the original filename. - Versioning: When a file is re‑uploaded, a new
file_recordis created; the old record is markedisobsolete, preserving history for rollbacks or audit.
3. Trust & Data Boundaries
Uploaded files are untrusted input. Trust boundaries are enforced at two stages:
- Upload validation:
file_upload()checks MIME type, runs virus‑scan hooks, and applies size limits. Only after passing these checks does the file reachfile_storage::save_file(). - Serving protection:
pluginfile.phpperforms a capability check against the file’s context. If the user lacksmoodle/file:download, the request is denied even if the file exists on disk.
Files are stored outside the web root or are served through pluginfile.php, preventing direct URL access and execution. Any plugin that writes to the filesystem bypassing the API breaks this boundary and must be audited.
4. Operational Checks
Routine health checks keep the file subsystem in sync:
- Quota & cleanup:
php admin/cli/check_file_usage.phpcompares thefilestable to physical files, reporting orphaned records. - Cron job:
cron.phprunsfile_storage::clean_orphaned_files()to delete stale data. - Health logs: Enable
debuggingand watch forMISSING_FILEorSTORAGE_BACKEND_UNAVAILABLEwarnings.
# Check file usage
php admin/cli/check_file_usage.php --dataroot=/var/moodledata
Expected output: counts of database records vs. physical files; any discrepancy triggers an alert.
5. Failure Modes & Design‑Change Conditions
- Backend outage: If the storage backend (e.g., S3) is unreachable,
file_storagereturns a 503 error; the request is logged withSTORAGE_BACKEND_UNAVAILABLE. - Permission drift: If the web server grants read access to
$CFG->dataroot/filedir, a user could download files directly, bypassing capability checks. This forces a redesign to enforce script‑only serving. - Plugin mis‑behaviour: A third‑party plugin writing files outside the API will bypass validation; if discovered, the plugin must be refactored to use
file_storage::save_file(). - Version upgrade: Moving from Moodle 3.8 to 3.9 introduces the external storage plugin interface; failing to migrate plugins can break file access.
6. Practical Example: Enabling AWS S3 Storage
Assume you want to offload user uploads to S3 while keeping the API unchanged.
- Install the
local_s3plugin (or any S3 storage plugin). The plugin registers a new backend classfile_storage_s3. - Edit
$CFG->filebaseinconfig.phpto point to the S3 bucket:$CFG->filebase = 's3://my-moodle-bucket'; - Configure AWS credentials in
config.phpor via environment variables. - Run
php admin/cli/upgrade.phpto apply any schema changes. - Verify that new uploads are stored in S3 by checking the bucket’s object list.
Operational check: run php admin/cli/check_file_usage.php after uploading a file. The command should report no orphaned files, confirming that the backend is correctly reporting file sizes and paths.
7. Checklist for Auditing the File Subsystem
| Check | Tool/Command | Expected Result |
|---|---|---|
| Validate MIME and size limits | Upload via UI and inspect debug logs for INVALID_FILE_TYPE or FILE_TOO_BIG | No upload passes if limits are exceeded |
| Capability enforcement | Attempt to download a file as a role without moodle/file:download | 403 Forbidden response |
| Storage backend health | Simulate S3 outage (disable network) | 503 Service Unavailable and error log entry |
| Orphaned file detection | php admin/cli/check_file_usage.php | Zero mismatches reported |
Conclusion
By keeping the file API as a thin, well‑defined interface and delegating storage to pluggable backends, Moodle achieves security, flexibility, and maintainability. Regular operational checks and clear trust boundaries protect against accidental exposure or data loss. When extending storage or upgrading Moodle, review the design change conditions to avoid breaking the file subsystem’s integrity.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.