Files
im.files handles uploading and downloading files: uploading avatars and group files, or uploading first and building your own message; turning file URLs in messages into download URLs you can load; listing and deleting group files; and recording voice.
You don't need these methods to send image, voice, video or file messages. Pass the file straight to im.messages.send(), and the SDK handles preprocessing, uploading and sending, and resumes after a page reload; see Messages. The methods on this page are for everything outside messages.
Rules for file purposes, size limits, retention and format checks are the same as on the server; see Files overview in the server docs.
| You want to | See |
|---|---|
| Upload avatars or group files, show progress, cancel | Upload a file, Cancel an upload |
| Preview a compressed image or get a video's cover before uploading | Use the preprocessing result on its own |
| Show images in messages, play video and voice | File URLs and download URLs |
| Let users save a file locally | Download file content |
| List, upload and delete group files | Group files |
| Hold to talk | Record voice |
Purposes and limits
Every upload must specify a purpose, which decides the allowed types, the size limit and who can download it:
purpose | Used for | Allowed kind | Size limit | After upload you get |
|---|---|---|---|---|
attachment | Images, voice, video and files in messages | image, voice, video, file | Runtime policy max_upload_bytes (100 MB by default); images also at most 20 MB, voice at most 5 MB | A file URL, which must be exchanged for a download URL before loading |
user_avatar | The user's own avatar | image | 5 MB | A public URL that loads directly |
group_avatar | Group avatar | image | 5 MB | A public URL |
chatroom_avatar | Chat room cover | image | 5 MB | A public URL |
group_file | Group files | image, video, file | Same as attachment | A file URL |
Before uploading, the SDK checks purpose, type and size locally. Files that don't qualify aren't uploaded, and the call rejects with local_validation: details.reason is too_large (with details.max_bytes as the limit) when the file is too big, or invalid_purpose when purpose and type don't match. The server also checks the app's storage quota and rejects with limit_exceeded (details.reason storage_quota) when it's full.
If the deployment has no file service (media_url_prefix in the runtime configuration is null), uploads reject with unsupported.
Upload a file
im.files.upload() returns an upload task: a Promise that resolves to a FileInfo, with an extra cancel() method.
async function uploadFile(file: File) {
const info = await im.files.upload(file, {
purpose: 'attachment',
kind: 'file',
onProgress: (loaded, total) => render(`${Math.round((loaded / total) * 100)}%`),
});
console.log(info.url, info.size);
}- Type. If
kindis omitted, it's taken from the file's MIME type:image/*isimage,video/*isvideo,audio/*isvoice, and anything else isfile. - File name. If
nameis omitted,File.nameis used. The SDK cleans the name the same way the server does: control characters,/,\and text-direction characters are removed, long names are cut to 255 characters, and an empty name becomes a default (such asfile.pdf).file-type files and group files are saved under this name when downloaded. - Progress.
onProgress(loaded, total)reports bytes.totalis the size actually uploaded after preprocessing (a compressed image may be smaller than the original). - In the returned
FileInfo,urlis the file URL (a public URL for avatars), and images also have a thumbnail URLthumbnail_url. Rely on itssize,widthandheight.
Uploading only stores the file; it doesn't send a message or change any profile. To set avatars, see Users, friends and presence and Groups.
Cancel an upload
Call the task's cancel(), or pass a signal and abort it. The task rejects with aborted, and the SDK also tells the server to cancel the upload.
const controller = new AbortController();
const task = im.files.upload(file, { purpose: 'attachment', signal: controller.signal });
cancelButton.onclick = () => task.cancel(); // 或 controller.abort()
try {
const info = await task;
} catch (err) {
if (err instanceof DRError && err.code === 'aborted') showToast('已取消');
}Large files and interruptions
- Files up to 32 MB are uploaded in one request. Larger files are uploaded in 16 MB parts, 3 at a time. Images, voice and avatars are always uploaded in one request.
- File content goes from the browser straight to the storage service, not through the IM service's API, and the requests carry no IM credentials.
- Network interruptions. Each content upload retries 3 times on network errors. If the server reports missing content when completing the upload, the SDK re-uploads the missing parts and then completes. When the server is busy or rate-limited (with a wait of 60 seconds or less), the SDK waits and retries. The task only resolves or rejects once there's a final result.
- Page reload or close.
upload()runs in the tab that called it and its state isn't saved. If the page reloads or closes, the upload stops and can't resume; upload again. Unfinished uploads are cleaned up by the server once they expire (they don't count toward storage). Attachments sent with messages are different: they're kept in the send queue and resume where they stopped when the page reopens; see Messages.
Preprocessing before upload
For upload() and when sending messages, the SDK preprocesses files before uploading:
| Content | Processing |
|---|---|
| Images | Rotated upright according to EXIF orientation; scaled down so the long side is at most 2048 pixels; re-encoded as JPEG (quality 0.85), or as PNG for PNG and WebP to keep transparency. Re-encoded output has no EXIF or other metadata (such as location). GIFs aren't compressed |
Images (original: true) | Not scaled or re-encoded. Only JPEG EXIF (except orientation) and XMP, and PNG text chunks, are removed; color profiles are kept. Formats the server doesn't accept (BMP, TIFF, AVIF and so on) are still converted to JPEG or PNG |
| HEIC, HEIF | Converted to JPEG if the browser can decode them (such as Safari); otherwise they can't be uploaded as images |
| Video | Not transcoded. Location and similar metadata is removed from MP4 and MOV (the file size doesn't change). Width, height and duration are read, and the first frame is captured as a cover |
| Voice | Not transcoded; only the format is checked |
| Files | Uploaded as is |
Formats are detected from the file's content, not its extension. The server accepts images in JPEG, PNG, GIF and WebP; video in MP4, MOV, WebM and 3GP; voice in AAC, M4A, MP3, AMR, 3GP, Ogg, WAV and WebM.
In the following cases the file can't be uploaded as the requested type, and upload() rejects with local_validation. You decide whether to upload again with kind: 'file' (for attachments sent with messages, the SDK switches to a file message automatically):
details.reason | Case |
|---|---|
image_too_large | The image is over 50 megapixels, or still over 20 MB after processing |
unsupported_type | The server doesn't accept the format, such as MKV or AVI video, or HEIC the browser can't decode |
async function uploadImage(file: File) {
try {
return await im.files.upload(file, { purpose: 'attachment', kind: 'image' });
} catch (err) {
if (err instanceof DRError && err.code === 'local_validation' && (err.reason === 'image_too_large' || err.reason === 'unsupported_type')) {
return im.files.upload(file, { purpose: 'attachment', kind: 'file' }); // 作为文件上传
}
throw err;
}
}Use the preprocessing result on its own
im.files.prepare() only does the processing above, without uploading. Use it to preview a compressed image, get a video's cover and duration, or decide before uploading whether to send as a file:
async function preview(file: File) {
const prepared = await im.files.prepare(file, 'video');
if (prepared.kind !== 'video') {
showToast('这个视频格式不能在线播放,将作为文件发送');
return;
}
render(prepared.width, prepared.height, prepared.duration_seconds);
if (prepared.thumbnail) render(URL.createObjectURL(prepared.thumbnail));
}prepare() doesn't reject files that can't be processed as the requested type. Instead it suggests a type (file) in the result's kind, with the reason in downgraded. upload() doesn't upload a video's cover; if you need one, upload thumbnail separately with kind: 'image'.
Show and download files
File URLs and download URLs
URLs of message attachments and group files (file URLs) can't be loaded directly; exchange them for short-lived download URLs first. Public avatar URLs and your own external URLs can be used as is. im.files.isFileUrl(url) tells you whether a URL is a file URL that needs exchanging:
async function srcOf(url: string): Promise<string> {
if (!im.files.isFileUrl(url)) return url;
const { download_url } = await im.files.resolveUrl(url);
return download_url;
}
async function showImage(img: HTMLImageElement, body: { url: string; thumbnail_url?: string }) {
img.src = await srcOf(body.thumbnail_url || body.url); // 列表中先显示缩略图
}- Thumbnails. An image's thumbnail URL (the file URL plus
/thumb, i.e.thumbnail_urlinFileInfoand image messages) also needs exchanging, and gives a thumbnail whose long side is at most 480 pixels. - Lifetime. Download URLs are valid for 60 to 90 minutes;
expires_atis when they expire. The SDK caches them until then and exchanges again when called within 5 minutes of expiry. Tabs in the same browser share this cache. Don't store download URLs or put them in messages; store the unchanging file URL and callresolveUrleach time you display it. - Batched requests. Exchanges within 20 ms are merged into one request (up to 100 each), so calling
resolveUrlfor each item in a list is fine. You can also useresolveUrlsto exchange several at once, with a result per item. - Each user can exchange up to 600 files per minute. When an item is rate-limited, the SDK retries it after 5 to 10 seconds.
Error codes when an exchange fails:
code | Description | Suggested message |
|---|---|---|
file_expired | The file has expired or been deleted | File expired |
not_found | The URL is invalid, or the file was deleted long ago | File expired |
file_blocked | The file was blocked for violating the rules | File blocked |
not_group_member | A group file, and the user is no longer a member | You're no longer in this group |
const results = await im.files.resolveUrls(urls);
for (const r of results) {
if (r.error) render(r.url, r.error.code === 'file_blocked' ? '文件已被屏蔽' : '文件已过期');
else render(r.url, r.download_url);
}Download file content
im.files.fetchBlob() exchanges the download URL and fetches the file content, with progress and cancellation. URLs that aren't file URLs are requested directly. Requests carry no credentials or cookies.
const blob = await im.files.fetchBlob(fileUrl, {
onProgress: (loaded, total) => render(total ? `${Math.round((loaded / total) * 100)}%` : `${loaded} 字节`),
});
// 让浏览器保存到本地
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = fileName;
a.click();
URL.revokeObjectURL(a.href);Download URLs for file-type files and group files carry Content-Disposition: attachment and the original file name. For large files you can also hand the download_url from resolveUrl to an <a href> or a new window and let the browser download it, without reading it into memory. For video and voice, set the download URL directly as the src of <video> or <audio>; download URLs support Range requests, so seeking works.
Group files
Group files are shared among group members. By default they don't expire (the app can set a retention period), and they're all deleted when the group is dissolved. Members can upload; the uploader, the owner and admins can delete. If the app turns off group files in its runtime policies (group_file_enabled is false in the runtime configuration), clients can't upload and upload() rejects with permission_denied (details.reason group_file_disabled).
// 上传:purpose 为 group_file,必须带 group_id;不能是语音
const info = await im.files.upload(file, { purpose: 'group_file', group_id: groupId });
// 列表:按上传时间从新到旧
const page = await im.files.groupFiles(groupId, { limit: 50 });
render(`共 ${page.file_count} 个文件,${(page.used_bytes / 1024 / 1024).toFixed(1)} MB`);
for (const f of page.items) render(f.name, f.size, f.owner_nickname, f.created_at);
// 删除
await im.files.deleteGroupFile(groupId, info.file_id);- Each group can have up to 1000 group files. Only current members can view the list and download; after leaving the group, the user can no longer download.
- Group file changes aren't pushed in real time; query again when the group files page opens.
- Group file URLs also need
resolveUrlto get a download URL.
Record voice
im.files.recordVoice() creates a recorder and picks the format based on what the browser supports. Browsers that can record audio/mp4 record M4A (AAC); others (such as Firefox and Chromium on Linux) record Opus in Ogg or WebM. The server accepts all of these, and SDKs on other platforms can play them.
const recorder = await im.files.recordVoice();
try {
await recorder.start(); // 第一次使用时浏览器会请求麦克风权限
} catch (err) {
if (err instanceof DRError && err.code === 'permission_required') showToast('请允许使用麦克风');
else if (err instanceof DRError && err.code === 'device_error') showToast('没有找到可用的麦克风');
throw err;
}
// 用户松开按钮时
const { blob, duration_seconds } = await recorder.stop();
await im.messages.send({ to_user: peer }, { type: 'voice', file: blob, body: { duration_seconds } });
// 用户上滑取消时改为调用
// recorder.cancel();- A recorder can only be used once:
stop()orcancel()releases the microphone, and to record again callrecordVoice()again. duration_secondsis the recording length rounded up, at least 1 second. The recorder doesn't limit duration, but voice files can't exceed 5 MB, so cap the recording length in your UI (for example 60 seconds).- Browsers only allow the microphone on secure pages (
httpsorlocalhost). Where recording isn't available,recordVoice()rejects withunsupported.
API reference
im.files.upload()
Uploads a file, preprocessing it first as described in Preprocessing before upload.
| Parameter | Type | Required | Description |
|---|---|---|---|
file | Blob | Yes | The file, usually a File from <input type="file"> |
p.purpose | string | Yes | Purpose: attachment, user_avatar, group_avatar, chatroom_avatar, group_file |
p.kind | FileKind | No | Type; taken from the MIME type by default. The three avatar purposes only allow image; group files can't be voice |
p.name | string | No | File name; defaults to File.name |
p.group_id | string | Required for group files | Group ID; not allowed for other purposes |
p.original | boolean | No | Don't compress images; only strip metadata |
p.onProgress | (loaded: number, total: number) => void | No | Upload progress |
p.signal | AbortSignal | No | For cancelling |
Returns: UploadTask, which resolves to a FileInfo.
Errors:
code | Description |
|---|---|
local_validation | details.reason: too_large over the size limit; invalid_purpose purpose and type don't match; required (details.field is group_id) group ID missing for a group file; invalid_value group_id given for another purpose; image_too_large, unsupported_type can't be uploaded as the requested type |
unsupported | The deployment has no file service |
permission_denied | The app turned off group files (group_file_disabled) |
aborted | Cancelled |
limit_exceeded | The app's storage quota is full (storage_quota), or the group file limit is reached |
rate_limited | Too many uploads, with a wait longer than 60 seconds (usually the daily quota is used up) |
invalid_argument | Failed the server's checks, such as unsupported_type or invalid_image |
not_found, not_group_member | The group doesn't exist, or the user isn't a member |
im.files.isFileUrl()
Whether a URL is a file URL (including thumbnail URLs) that needs exchanging for a download URL. Runs synchronously.
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | URL |
Returns: boolean. Always false when the deployment has no file service.
im.files.resolveUrl()
Exchanges a file URL for a download URL, caching the result until it expires.
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | File URL or thumbnail URL |
Returns: Promise<{ download_url: string; expires_at: string }>.
Errors: file_expired, file_blocked, not_found, not_group_member, and network errors.
im.files.resolveUrls()
Exchanges several file URLs for download URLs, with a result per item.
| Parameter | Type | Required | Description |
|---|---|---|---|
urls | string[] | Yes | File URLs |
Returns: Promise<ResolvedUrl[]> in request order.
im.files.fetchBlob()
Gets a file's content. File URLs are exchanged for download URLs first; other URLs are requested directly.
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | URL |
p.onProgress | (loaded: number, total: number) => void | No | Download progress; total is 0 if the server doesn't give a size |
p.signal | AbortSignal | No | For cancelling |
Returns: Promise<Blob>.
Errors: errors from exchanging the download URL; not_found (the download URL returned 404), download_failed, network_error, aborted.
im.files.groupFiles()
Lists group files, newest upload first.
| Parameter | Type | Required | Description |
|---|---|---|---|
group_id | string | Yes | Group ID |
page.cursor | string | null | No | Cursor for the next page |
page.limit | number | No | Items per page; default 20, max 100 |
Returns: Promise<Page<GroupFile> & { file_count: number; used_bytes: number }>. file_count and used_bytes are the group's total number of group files and the bytes they use.
Errors: not_found, not_group_member.
im.files.deleteGroupFile()
Deletes a group file: the uploader, the owner or an admin. Succeeds if already deleted.
| Parameter | Type | Required | Description |
|---|---|---|---|
group_id | string | Yes | Group ID |
file_id | string | Yes | File ID |
Returns: Promise<void>.
Errors: permission_denied (not_uploader: not the uploader, owner or an admin), not_found, not_group_member, file_blocked (blocked files can't be deleted).
im.files.prepare()
Processes a file by the preprocessing rules, without uploading. Doesn't require sign-in.
| Parameter | Type | Required | Description |
|---|---|---|---|
file | Blob | Yes | The file |
kind | FileKind | Yes | The desired type |
p.original | boolean | No | Don't compress images; only strip metadata |
Returns: Promise<PreparedFile>.
Errors: local_validation (file isn't a Blob).
im.files.recordVoice()
Creates a voice recorder.
Returns: Promise<VoiceRecorder>.
Errors: unsupported (the browser can't record).
VoiceRecorder.start()
Requests the microphone and starts recording.
Returns: Promise<void>.
Errors: permission_required (the user didn't allow the microphone), device_error (no microphone, or it's in use).
VoiceRecorder.stop()
Stops recording and releases the microphone.
Returns: Promise<{ blob: Blob; duration_seconds: number }>.
Errors: invalid_state (details.reason is not_recording: not recording).
VoiceRecorder.cancel()
Discards the recording and releases the microphone. Runs synchronously.
Types
FileInfo
File information after an upload completes.
| Field | Type | Description |
|---|---|---|
file_id | string | File ID |
purpose | string | Purpose |
kind | FileKind | Type |
status | string | Status; active once uploaded |
url | string | A file URL for message attachments and group files; a public URL for avatars |
thumbnail_url | string | null | Thumbnail URL; only for image attachments and image group files |
name | string | null | File name |
content_type | string | Format detected by the server from the content, such as image/jpeg |
size | number | Size in bytes |
width | number | null | Width of images and avatars, in pixels |
height | number | null | Height of images and avatars, in pixels |
owner | string | null | The uploader's username |
group_id | string | null | The group a group file belongs to |
via | string | How it was uploaded; client for client uploads |
created_at | string | Upload time; retention counts from here |
expires_at | string | null | Expiry time; set for message attachments and for group files with a retention period |
GroupFile
A group file. In addition to the FileInfo fields:
| Field | Type | Description |
|---|---|---|
owner_nickname | string | The uploader's nickname |
owner_avatar_url | string | The uploader's avatar |
UploadParams
The second argument to upload(); for its fields, see im.files.upload() in the API reference.
UploadTask
A Promise<FileInfo> with one extra method:
| Method | Description |
|---|---|
cancel() | Cancels the upload; the task rejects with aborted |
FileKind
'image' | 'voice' | 'video' | 'file'.
PreparedFile
| Field | Type | Description |
|---|---|---|
blob | Blob | The processed content to upload |
kind | FileKind | The suggested type; file if it can't be processed as the requested type |
name | string | The processed file name; the extension changes when an image is re-encoded |
format | string | Lowercase format name, such as jpeg, png, mp4; the extension for file |
content_type | string | MIME type |
width | number | Width of images and video; absent if it can't be read |
height | number | Height of images and video |
duration_seconds | number | Video duration in seconds |
thumbnail | Blob | The video's first frame as a JPEG |
downgraded | string | Why kind became file: image_too_large or unsupported_type |
ResolvedUrl
| Field | Type | Description |
|---|---|---|
url | string | The requested file URL |
download_url | string | Download URL; absent on failure |
expires_at | string | When the download URL expires |
error | DRError | The error on failure; see File URLs and download URLs |
VoiceRecorder
| Member | Type | Description |
|---|---|---|
format | 'm4a' | 'aac' | 'ogg' | 'webm' | Recording format, fixed when the recorder is created |
start() | () => Promise<void> | Starts recording |
stop() | () => Promise<{ blob: Blob; duration_seconds: number }> | Stops recording |
cancel() | () => void | Discards the recording |
