Storage
Loopar ships a pluggable storage layer. Every uploaded file goes through a storage driver; you can keep files on the server (default), push them to a cloud provider like Cloudinary, or reference external URLs without storing bytes at all.
How It Works
| Piece | Role |
|---|---|
loopar.storage | Driver registry (StorageManager) |
| Active driver | Receives all new uploads |
| Per-file driver | Every file records the driver that stored it ā reads always go through the owning driver |
File Manager | The file record: name, size, visibility, storage_driver, external refs |
Because each file remembers its own driver, switching the active driver is always safe: existing assets keep being served by the driver that stored them, only new uploads go to the new provider.
Upload ā active driver (local / cloudinary / ...)
Read ā loopar.storage.for(file.storage_driver) ā the owner, not the active one
Drivers
| Driver | What it does |
|---|---|
local | Default, always registered. Stores binaries under the app/tenant uploads/{visibility}/ folder and serves them at /assets/{visibility}/{name}. Generates thumbnails when sharp is available. |
cloudinary | Uploads to Cloudinary via its HTTP API; the CDN URL is recorded as external_url. |
reference | Not a real driver ā an import mode. The file is just a pointer to an external URL: no bytes are stored, external_id = the URL. |
Driver contract
Custom providers implement the StorageDriver contract:
class MyDriver extends StorageDriver {
get name() { return "my-provider"; }
async upload({ buffer, originalName, app, visibility, contentType }) {
// ā { externalId, src, bytes, format, width, height, storedName }
}
async importFromUrl({ url, app, visibility }) { /* ... */ }
async delete({ externalId, storedName, app, visibility }) { /* ... */ }
deliveryUrl({ externalId, src, transform }) { /* ... */ }
}
Configuration
Cloudinary
Fill in the Cloudinary Single entity (Desk ā Integrations):
| Field | Description |
|---|---|
cloud_name | Your Cloudinary cloud name (required) |
api_key | API key (required) |
api_secret | API secret (required, stored as password) |
folder_root | Folder prefix in Cloudinary (default loopar) |
Activating a driver
Set active_storage in System Settings to the name of the integration document (e.g. Cloudinary). On boot, Loopar loads that document, calls its buildDriver() and activates the driver.
- Saving the integration document re-applies storage automatically (
loopar.applyStorage()) ā no restart needed. - If the active driver is misconfigured or missing, Loopar falls back to
localand keeps working.
System Settings.active_storage = "Cloudinary"
ā
ā¼
getDocument("Cloudinary").buildDriver() ā storage.activateDriver(driver)
ā (on any failure)
ā¼
fallback: local
File Manager & Mirrors
File Manager is a filesystem-backed entity ā file records don't live in the database. Each stored file gets a sidecar mirror, {name}.meta.json, written atomically next to the asset:
{
"version": 1,
"name": "team-photo.jpg",
"type": "image/jpeg",
"size": 84213,
"storage_driver": "cloudinary",
"external_id": "loopar/team-photo",
"external_url": "https://res.cloudinary.com/...",
"src": "https://res.cloudinary.com/...",
"app": "my-app",
"visibility": "public"
}
| Field | Purpose |
|---|---|
storage_driver | Which driver owns this file (local, cloudinary, reference) |
external_id / external_url | Provider identifiers (null for local) |
visible | public or private (private files are permission-checked) |
app | App scope ā assets travel with the app installer |
Serving
Assets are served from /assets/{public|private}/{filename} (thumbnails under /assets/{visibility}/thumbnails/). The router resolves the mirror across all asset roots (tenant + apps) and redirects/streams from the owning driver.
Deduplication
Uploading a file with the same name into the same app reuses the existing asset ā the mirror is detected and no second upload happens.
Uploads from the UI
The form elements image_input, file_input and file_uploader handle uploads for you:
- The user picks a file in the form.
- On document save, Loopar creates the
File Managerrecord and hands the bytes to the active driver. - The field stores a
file_refand the asset becomes available at/assets/{visibility}/{name}.
Remote imports work the same way: paste a URL and choose whether to download it (bytes go through the driver) or keep it as a reference (URL pointer only).
{ "element": "image_input", "data": { "name": "cover_image", "label": "Cover" } }