Loopar Introduction
loopar-webpage

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

PieceRole
loopar.storageDriver registry (StorageManager)
Active driverReceives all new uploads
Per-file driverEvery file records the driver that stored it — reads always go through the owning driver
File ManagerThe 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

DriverWhat it does
localDefault, always registered. Stores binaries under the app/tenant uploads/{visibility}/ folder and serves them at /assets/{visibility}/{name}. Generates thumbnails when sharp is available.
cloudinaryUploads to Cloudinary via its HTTP API; the CDN URL is recorded as external_url.
referenceNot 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):

FieldDescription
cloud_nameYour Cloudinary cloud name (required)
api_keyAPI key (required)
api_secretAPI secret (required, stored as password)
folder_rootFolder 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 local and 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"
}
FieldPurpose
storage_driverWhich driver owns this file (local, cloudinary, reference)
external_id / external_urlProvider identifiers (null for local)
visiblepublic or private (private files are permission-checked)
appApp 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:

  1. The user picks a file in the form.
  2. On document save, Loopar creates the File Manager record and hands the bytes to the active driver.
  3. The field stores a file_ref and 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" } }