useUploader()
useUploader() wires framework's built-in createUploader factory to the current project's
SystemStore (endpoint URLs) and UserStore (current user ID), returning a ready-to-use
uploader instance. You never pass endpoint / token / userId — each call reads fresh values
from the stores to avoid stale closures.
Prerequisites
- Project has framework installed (
modules: ['@swift-web/framework']) and the user is logged in- Backend has returned
system/infocontainingupload_image_url,upload_file_url,upload_file_query_url,upload_file_max_length,upload_image_max_length(real endpoints are fully driven by backend systemInfo)- Browser-only (depends on
File/FormData/Blob); do not call during SSR
Basic Usage
<script setup lang="ts">
const uploader = useUploader()
async function handleAvatar(file: File) {
// Second arg `thumbnail`: true=generate thumbnail, false=original
const { id, value, width, height } = await uploader.uploadImage(file, false)
// `value` is the backend URL; `id` is for business-side references
console.log({ id, value, width, height })
}
</script>
Return Value
useUploader() returns an object with four methods:
| Method | Signature | Purpose |
|---|---|---|
uploadImage | (file: File, thumbnail: boolean) => Promise<ImageUploadResult> | Single POST upload of an image; pre-validates size locally |
uploadVideo | (files: File[], options?: VideoUploadOptions) => void | Concurrent chunked upload of videos (2 MB / chunk, 3 retries) |
uploadFile | (file: File, options?: FileUploadOptions) => Promise<FileUploadResult> | Chunked upload of a plain file (PDF / doc / zip), no transcoding |
queryVideo | (id: string, retries?: number) => Promise<VideoQueryResult> | Poll backend transcoding (every 3s, up to 20 times by default) |
uploadImage
Single POST request. Locally pre-validates byte size (images use upload_image_max_length, falling back to upload_file_max_length when unset)
(throws Error('文件超出大小') if exceeded). Returns:
type ImageUploadResult = {
id: string // Backend file ID (business-side foreign key)
value: string // File access URL
width: number // Parsed from backend file_info "1280x720"
height: number
}
<script setup lang="ts">
const uploader = useUploader()
async function onPick(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0]
if (!file) return
try {
const cover = await uploader.uploadImage(file, false)
form.value.cover = { id: cover.id, url: cover.value, w: cover.width, h: cover.height }
} catch (err) {
// Possible errors: '文件超出大小' / backend status='n' / '图片尺寸解析失败:file_info="..."'
console.error(err)
}
}
</script>
uploadVideo
Callback-based — returns void immediately. Spawns one UploadChunk instance per File;
files upload concurrently and independently.
type VideoUploadOptions = {
retries?: number // Max retries per chunk, default 3
chunkSize?: number // Chunk size in bytes, default 1024 * 1024 * 2 (2 MB)
onBefore?: (file: File) => void
onProcess?: (r: ChunkProgress) => void
onSuccess?: (r: ChunkSuccess) => void
onError?: (r: ChunkError) => void
}
interface ChunkProgress { file: File; processed_num: number; chunk_total: number; chunk_index: number; file_md5: string }
interface ChunkSuccess { file: File; id: string; file_md5: string }
interface ChunkError { file: File; error: unknown; chunk_total: number; chunk_index: number; file_md5: string }
Typical flow: upload video → get id → call queryVideo to wait for transcoding → write to form.
<script setup lang="ts">
const uploader = useUploader()
const progress = ref(0)
const videoMeta = ref<Awaited<ReturnType<typeof uploader.queryVideo>> | null>(null)
function pickVideo(file: File) {
uploader.uploadVideo([file], {
onProcess: ({ processed_num, chunk_total }) => {
progress.value = Math.round((processed_num / chunk_total) * 100)
},
onSuccess: async ({ id }) => {
videoMeta.value = await uploader.queryVideo(id)
},
onError: ({ error }) => {
console.error('Video upload failed', error)
},
})
}
</script>
uploadFile
Upload a plain file (PDF / document / zip — anything that isn't an image or video). It reuses
the same chunked endpoint (upload_file_url) as uploadVideo, but without the transcoding
step — the backend's chunk-success response already carries an accessible URL, so no queryVideo
is needed. Like uploadImage, it returns a Promise you await for the result, and rejects with
an Error on failure.
type FileUploadOptions = {
chunkSize?: number // Chunk size in bytes, default 1024 * 1024 * 2 (2 MB)
retries?: number // Max retries per chunk, default 3
concurrency?: number // Concurrent chunks; defaults to 6 when omitted
onProcess?: (r: ChunkProgress) => void // Progress callback (reuses upload-chunk's ChunkProgress)
onBefore?: (file: File) => void // Called before upload starts (before md5 computation)
}
type FileUploadResult = {
id: string // Backend file ID (business-side foreign key)
value: string // File access URL (same `value` semantics as uploadImage)
file_md5: string // Whole-file MD5
}
<script setup lang="ts">
const uploader = useUploader()
const progress = ref(0)
async function pickFile(file: File) {
try {
const { id, value } = await uploader.uploadFile(file, {
onProcess: ({ processed_num, chunk_total }) => {
progress.value = Math.round((processed_num / chunk_total) * 100)
},
})
form.value.attachment = { id, url: value }
} catch (err) {
// Terminal failure: network error / backend status='n' / empty file, etc. — all reject Error
console.error('File upload failed', err)
}
}
</script>
uploadFile vs uploadVideo: same endpoint and chunking mechanism; the difference is how you get the result —
uploadFilehas no transcoding and youawaitthe URL, whileuploadVideouses callbacks plus a follow-upqueryVideoto wait for transcoding. UseuploadImagefor images,uploadVideofor videos, anduploadFilefor everything else.
queryVideo
After a video upload succeeds, backend transcoding takes time. queryVideo polls every 3
seconds until file_m3u8 is available or retries are exhausted.
type VideoQueryResult = {
id: string
file_m3u8: string // Playable URL after transcoding
file_duration: string
file_preview_m3u8: string
file_status: string
width: number // Parsed from file_quality "1920x1080"
height: number
}
// Default 20 retries (~60s); throws Error('查询重试次数已用尽') on exhaustion
const result = await uploader.queryVideo(id, 30)
Notes
- Endpoint URLs are fully driven by backend
system/info— no frontend config option. To swap upload backends locally, mocksystem/info. uploadVideois not a Promise — it notifies results viaonSuccess/onErrorcallbacks. Wrap it in a Promise if you needawait.- Individual chunk failures are retried internally by
UploadChunkup toretries; business code seesonErroronly on terminal failure. queryVideosilently swallows per-attempt errors (only rejects after retries are exhausted). Add request-level logging in dev to see real HTTP failures.- Upload timeout is controlled by
runtimeConfig.public.swiftWebFramework.request.uploadTimeout, default600_000(10 minutes).