๐ค 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.