📤 File Upload & Media Components
Status: Implemented and documented (Feb 2026)
📖 Overview
Comprehensive file upload system with image editing, asset picking, and multi-file support.
🧱 Components
⬆️ FileUploader (ui/media/FileUploader.svelte)
Multi-file uploader with:
- Drag & drop support
- Click to browse
- File type & size validation
- Edit (✏️) per pending file — on an image the button is titled Edit: it dispatches
editImageand the host opens ImageEditModal. On any other file it is titled Rename: it dispatcheseditFileand the host opens FileEditModal. The edited file replaces the pending one (replaceFile()). Nothing opens on its own: on Upload the host receives the pending files as they are (the Files page'spendingImageFilesqueue inroutes/(app)/files/+page.svelteis never filled) - Restore original (↩️) on every edited file, cropped or renamed (
editedIndices), Remove on every file - Preview for images via backend thumbnail API
Hosts:
- Files page, Static Resources —
on:uploadsends each file throughuploadFile(). - Files page, Broker Reports (
accept=".csv,.xlsx,.xls") — the selection opens the Assign Brokers dialog: one broker per file or Assign all to, pre-filled when the user can edit exactly one broker (Viewer brokers are left out), and Upload (n) enabled only once every file has a broker. All the files of one confirmation share abatch_id(generateUUID()), so they form one report set. - BrokerImportFilesModal — the report files of one broker.
✂️ ImageCropper (ui/media/ImageCropper.svelte)
cropperjs v2 based image cropper (Web Components):
- Free crop with L-shaped corner handles
- Zoom (overlay buttons and mouse wheel) works on the selection first: zoom-in shrinks it by
10% until it covers 50% of the image (
MIN_SELECTION_COVERAGE), then zooms the image by 0.1; zoom-out enlarges it by 10% up to 90% (MAX_SELECTION_COVERAGE), then zooms the image out - Rotation (15° steps, −15° / +15°) relative to selection center
- Flip horizontal/vertical
- Aspect ratio given by the host (
aspectRatio;0orNaN= free); the ratio buttons live in ImageEditModal - Selection clamped to canvas bounds
- Middle-mouse-button drag for background translation
🖼️ ImageEditModal (ui/media/ImageEditModal.svelte)
Full image editing modal (extends ModalBase):
It opens only when a host asks: the ✏️ Edit button of FileUploader (Files page), or the upload path of ImagePickerWrapper (below) — never by itself when a file is picked.
- Confirm button — Crop & Upload when
uploadOnCompleteistrue(the default, used by ImagePickerWrapper); Crop when it isfalse, as on the Files page after ✏️, where the cropped file goes back to the pending list - Presets (
IMAGE_PRESETSinlib/utils/files/imageCrop.ts): buttons Avatar (200×200), Icon (broker-icon, 64×64) and Custom (free size), shown whileallowPresetChange(defaulttrue). Theasset-iconpreset (256×256) has no button: only a host can set it - Round preview — a toggle for the ellipse overlay, on by default for Avatar and Icon
- Ratio buttons 1:1, 16:9, 4:3, 3:4 and Free, shown only with Custom
- Output — width and height are capped at the selection and keep its ratio; Scale goes from 0.01 to 1
- Format —
.png,.jpgor.webp, defaulting to the source file's type. Quality (JPEG and WebP only) moves with −/+ in 10-point steps from 10 to 100, default 90 - Reset All (header) puts zoom, rotation and flip back and re-centres the selection
(
resetAll()) - Closing with changes asks Discard changes? — Discard & Close
📄 FileEditModal (ui/media/FileEditModal.svelte)
Simple file rename modal (extends ModalBase):
- Rename file before upload
- Used for PDFs, CSVs, and other non-image files
- Also available for BRIM import files
🔎 AssetPickerModal (ui/media/AssetPickerModal.svelte)
3-tab asset picker modal (extends ModalBase):
- Existing: Browse uploaded files with DataTable or FileGrid
- URL: Enter a URL with live preview and circular overlay
- Upload: Upload + crop a new image
Used for: broker icons, user avatars, and any image URL field.
📸 ImagePickerWrapper (ui/media/ImagePickerWrapper.svelte)
(This component is an invisible logic wrapper and has no UI of its own) Wraps the full AssetPicker → ImageEdit flow:
- Opens AssetPickerModal
- If upload selected → opens ImageEditModal
- Returns final URL to parent
📁 FileGrid (files/FileGrid.svelte)
Shared grid view component used in both /files page and AssetPickerModal.
📂 Files
frontend/src/lib/components/
├── files/
│ └── FileGrid.svelte
└── ui/media/
├── AssetPickerModal.svelte
├── FileEditModal.svelte
├── FileUploader.svelte
├── ImageCropper.svelte
├── ImageEditModal.svelte
├── ImagePickerWrapper.svelte
├── ImageUploader.svelte
├── LazyImage.svelte
└── index.ts
🖥️ Backend Support
- Image preview API:
GET /api/v1/uploads/file/{id}?img_preview=WxH - Preview cache: Size-based (
PREVIEW_CACHE_MAX_MBin.env, default 50MB), TTL 1h, invalidated on delete - No-op for small images: If requested size ≥ original, serves file directly without processing
- Seed avatars: 30 default avatar PNGs copied to
custom-uploads/on first startup viaseed_default_avatars() - Upload utility: Centralized
uploadFile()inutils/upload.ts— single FormData creation point - Size formatting: Centralized i18n-aware
formatBytes()inutils/upload.ts
✅ Validation
Client side (FileUploader):
- File type checking against
acceptprop - File size checking against global
max_file_upload_mbsetting
Server side:
- Size —
get_max_upload_mb()(max_file_upload_mb, default 10) is enforced on bothPOST /uploadsandPOST /brokers/import/upload:413above the limit. - Static files (
validate_upload_security()inservices/static_uploads.py) — the extension must not be inBLOCKED_EXTENSIONS(.exe,.dll,.sh,.py,.js,.jar, …); whenpython-magic(libmagic) loads, the content's detected type must not be inBLOCKED_MIME_TYPES, otherwise the type is guessed from the extension (mimetypes). A declared type that differs from the detected one is only logged. A refused file returns400. - Static delete — only the uploader or a superuser (
403Cannot delete files uploaded by other users); the UI shows the action to everyone. - Broker reports — EDITOR or OWNER on the target broker (
403), an empty file is refused (400),batch_idmust be a UUID (422). There is no server-side extension check: only the uploader'sacceptlimits them to CSV and Excel.