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/info containing upload_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:

MethodSignaturePurpose
uploadImage(file: File, thumbnail: boolean) => Promise<ImageUploadResult>Single POST upload of an image; pre-validates size locally
uploadVideo(files: File[], options?: VideoUploadOptions) => voidConcurrent 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 — uploadFile has no transcoding and you await the URL, while uploadVideo uses callbacks plus a follow-up queryVideo to wait for transcoding. Use uploadImage for images, uploadVideo for videos, and uploadFile for 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, mock system/info.
  • uploadVideo is not a Promise — it notifies results via onSuccess / onError callbacks. Wrap it in a Promise if you need await.
  • Individual chunk failures are retried internally by UploadChunk up to retries; business code sees onError only on terminal failure.
  • queryVideo silently 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, default 600_000 (10 minutes).