文件
im.files 负责文件的上传和下载:上传头像、群文件,或先上传再自己组织消息;把消息中的文件地址换成可以加载的下载地址;查看和删除群文件;录制语音。
发送图片、语音、视频和文件消息时不需要先调用本页的方法:把文件直接交给 im.messages.send(),SDK 会完成上传前的处理、上传和发送,并在页面刷新后续传,见消息。本页的方法用于消息之外的场景。
文件的用途、大小上限、保留时间、格式检查等规则与服务端相同,见服务端的文件概述。
文件的用途与上限
每次上传都要指定用途 purpose,它决定允许的类型、大小上限和谁能下载:
purpose | 用于 | 允许的 kind | 大小上限 | 上传后得到 |
|---|---|---|---|---|
attachment | 消息中的图片、语音、视频、文件 | image、voice、video、file | 运行策略 max_upload_bytes(默认 100 MB);图片另不超过 20 MB,语音另不超过 5 MB | 文件地址,要换取下载地址才能加载 |
user_avatar | 本人的头像 | image | 5 MB | 公开地址,可以直接加载 |
group_avatar | 群头像 | image | 5 MB | 公开地址 |
chatroom_avatar | 聊天室的封面 | image | 5 MB | 公开地址 |
group_file | 群文件 | image、video、file | 同 attachment | 文件地址 |
上传前 SDK 先在本地检查用途、类型和大小,不符合的不会开始上传,以 local_validation 拒绝:大小超出时 details.reason 为 too_large,details.max_bytes 为上限;用途与类型不匹配时为 invalid_purpose。服务端还会检查应用的存储额度,额度已满时以 limit_exceeded(details.reason 为 storage_quota)拒绝。
应用所在的部署没有提供文件服务时(运行配置的 media_url_prefix 为 null),上传以 unsupported 拒绝。
上传文件
im.files.upload() 返回一个上传任务:它是兑现为 FileInfo 的 Promise,另外有 cancel() 方法。
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);
}- 类型:省略
kind时按文件的 MIME 类型判断:image/*为image,video/*为video,audio/*为voice,其他为file。 - 文件名:省略
name时用File.name。SDK 按服务端的规则处理文件名:去掉控制字符、/、\和改变文字方向的字符,超长的截断到 255 个字符,为空时用默认名(如file.pdf)。file类型和群文件下载时以这个名字保存。 - 进度:
onProgress(loaded, total)按字节报告,total是处理后实际上传的大小(图片压缩后可能比原文件小)。 - 返回的
FileInfo中,url是文件地址(头像为公开地址),图片另有缩略图地址thumbnail_url;size、width、height以它为准。
上传只得到文件,不发送消息,也不修改资料。设置头像见用户、好友与在线状态和群组。
取消上传
调用任务的 cancel(),或传入 signal 后中止它。任务以 aborted 拒绝,SDK 同时通知服务端取消这次上传。
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('已取消');
}大文件与中断后的处理
- 不超过 32 MB 的文件一次上传;更大的分片上传,每片 16 MB,同时上传 3 片。图片、语音和头像总是一次上传。
- 文件内容由浏览器直接传到存储服务,不经过 IM 服务的接口;请求不带 IM 的登录凭证。
- 网络中断:每次上传内容遇到网络错误时自动重试 3 次;完成上传时服务端报告缺少内容的,SDK 自动补传缺少的分片后再完成。服务端繁忙、限流(等待时间不超过 60 秒的)时 SDK 等待后重试,任务只在得到最终结果时兑现或拒绝。
- 页面刷新或关闭:
upload()在调用它的标签页中执行,上传状态不保存。页面刷新或关闭后上传中止,不能续传,需要重新上传;没有完成的上传由服务端过期后清理(不计入存储用量)。随消息发送的附件不同:它们保存在发送队列中,页面重新打开后从中断处续传,见消息。
上传前的处理
upload() 和发送消息时,SDK 在上传前自动处理文件:
| 内容 | 处理 |
|---|---|
| 图片 | 按 EXIF 的方向转正;长边超过 2048 像素的缩小到 2048;重新编码为 JPEG(质量 0.85),PNG 和 WebP 编码为 PNG 以保留透明。重新编码的结果不含 EXIF 等元数据(如拍摄地点)。GIF 不压缩 |
图片(original: true) | 不缩小、不重新编码,只去掉 JPEG 的 EXIF(保留方向)和 XMP、PNG 的文本块,保留颜色配置。服务端不接受的格式(BMP、TIFF、AVIF 等)仍转为 JPEG 或 PNG |
| HEIC、HEIF | 浏览器能解码的(如 Safari)转为 JPEG;不能解码的不能作为图片上传 |
| 视频 | 不转码。MP4 和 MOV 去掉拍摄地点等元数据(不改变文件大小);读出宽高和时长,截取第一帧作为封面 |
| 语音 | 不转码,只检查格式 |
| 文件 | 原样上传 |
格式按文件开头的内容判断,不看扩展名。服务端接受的格式:图片 JPEG、PNG、GIF、WebP;视频 MP4、MOV、WebM、3GP;语音 AAC、M4A、MP3、AMR、3GP、Ogg、WAV、WebM。
以下情况不能按请求的类型上传,upload() 以 local_validation 拒绝,由你决定是否改为 kind: 'file' 重新上传(随消息发送的附件由 SDK 自动改为文件消息):
details.reason | 情况 |
|---|---|
image_too_large | 图片超过 5000 万像素,或处理后仍超过 20 MB |
unsupported_type | 格式不被服务端接受,如 MKV、AVI 视频,或浏览器不能解码的 HEIC |
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;
}
}单独使用处理结果
im.files.prepare() 只做上面的处理,不上传。可以用它预览压缩后的图片、取得视频的封面和时长,或在上传前判断是否要改为文件:
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() 不会拒绝不能按请求类型处理的文件,而是在结果的 kind 中给出建议的类型(file),downgraded 说明原因。upload() 不会上传视频的封面;需要封面时,把 thumbnail 以 kind: 'image' 另外上传。
显示与下载文件
文件地址与下载地址
消息附件和群文件的地址(文件地址)不能直接加载,要先换成短期有效的下载地址。头像的公开地址、你自己的外部地址可以直接使用。im.files.isFileUrl(url) 判断一个地址是不是需要换取的文件地址:
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); // 列表中先显示缩略图
}- 缩略图:图片的缩略图地址(文件地址加
/thumb,即FileInfo和图片消息中的thumbnail_url)同样要换取,得到长边不超过 480 像素的缩略图。 - 有效期:下载地址的有效期为 60 到 90 分钟,
expires_at为到期时间。SDK 按到期时间缓存,到期前 5 分钟内再调用时重新换取;同一浏览器的多个标签页共用这个缓存。不要把下载地址保存下来或写进消息,保存不变的文件地址,每次显示时调用resolveUrl。 - 合并请求:20 毫秒内的多次换取合并成一次请求(每次最多 100 个),列表中逐个调用
resolveUrl即可。也可以用resolveUrls一次换取多个,每项单独给出结果。 - 换取每个用户每分钟最多 600 个文件,单项被限流时 SDK 在 5 到 10 秒后自动重试。
换取失败时的错误码:
code | 说明 | 建议的提示 |
|---|---|---|
file_expired | 文件已过期或已被删除 | 文件已过期 |
not_found | 地址无效,或文件已删除很久 | 文件已过期 |
file_blocked | 文件因违规被屏蔽 | 文件已被屏蔽 |
not_group_member | 群文件,本人已不是群成员 | 你已不在群中 |
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);
}下载文件内容
im.files.fetchBlob() 换取下载地址后取得文件内容,可以报告进度、中途取消;不是文件地址的直接请求这个地址。请求不带任何登录凭证和 Cookie。
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);file 类型的文件和群文件的下载地址带有 Content-Disposition: attachment 和原文件名,大文件也可以把 resolveUrl 得到的 download_url 直接交给 <a href> 或新窗口,由浏览器下载,不必读进内存。视频、语音可以直接把下载地址设为 <video>、<audio> 的 src,下载地址支持 Range 请求,可以拖动进度。
群文件
群文件是群成员共享的文件,默认不过期(应用可以设置保留天数),群解散时全部删除。群成员可以上传;上传者本人、群主和管理员可以删除。应用在运行策略中关闭了群文件(运行配置的 group_file_enabled 为 false)时,客户端不能上传,upload() 以 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);- 每个群最多 1000 个群文件。只有当前的群成员能查看列表和下载,离开群之后不能再下载。
- 群文件的变化没有实时通知,打开群文件页面时重新查询。
- 群文件的地址同样要用
resolveUrl换取下载地址。
录制语音
im.files.recordVoice() 创建一个录音器,按浏览器的能力选择格式:能录制 audio/mp4 的浏览器录为 M4A(AAC),其他浏览器(如 Firefox、Linux 上的 Chromium)录为 Ogg 或 WebM 封装的 Opus。这些格式服务端都接受,其他平台的 SDK 都能播放。
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();- 录音器只能使用一次:
stop()或cancel()之后释放麦克风,再录要重新调用recordVoice()。 duration_seconds按录音的时长向上取整,至少 1 秒。录音器不限制时长,语音文件不能超过 5 MB,请在界面上限制最长的录音时间(如 60 秒)。- 浏览器只在安全的页面(
https或localhost)中允许使用麦克风。没有录音能力的环境中recordVoice()以unsupported拒绝。
接口参考
im.files.upload()
上传一个文件。上传前按上传前的处理处理文件。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
file | Blob | 是 | 文件,通常是 <input type="file"> 得到的 File |
p.purpose | string | 是 | 用途:attachment、user_avatar、group_avatar、chatroom_avatar、group_file |
p.kind | FileKind | 否 | 类型,默认按 MIME 类型判断;三种头像只能是 image,群文件不能是 voice |
p.name | string | 否 | 文件名,默认为 File.name |
p.group_id | string | 群文件必填 | 群 ID;其他用途不能带 |
p.original | boolean | 否 | 图片不压缩,只去掉元数据 |
p.onProgress | (loaded: number, total: number) => void | 否 | 上传进度 |
p.signal | AbortSignal | 否 | 用于取消 |
返回值:UploadTask,兑现为 FileInfo。
可能的错误:
code | 说明 |
|---|---|
local_validation | details.reason:too_large 超出大小上限;invalid_purpose 用途与类型不匹配;required(details.field 为 group_id)群文件缺少群 ID;invalid_value 其他用途带了 group_id;image_too_large、unsupported_type 不能按请求的类型上传 |
unsupported | 部署没有提供文件服务 |
permission_denied | 应用关闭了群文件(group_file_disabled) |
aborted | 已取消 |
limit_exceeded | 应用的存储额度已满(storage_quota),或群文件数已满 |
rate_limited | 上传过于频繁,需要等待的时间超过 60 秒(通常是当天的额度已用完) |
invalid_argument | 服务端检查不通过,如 unsupported_type、invalid_image |
not_found、not_group_member | 群不存在,或本人不是群成员 |
im.files.isFileUrl()
判断一个地址是不是需要换取下载地址的文件地址(包括缩略图地址)。同步执行。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
url | string | 是 | 地址 |
返回值:boolean。部署没有提供文件服务时一律为 false。
im.files.resolveUrl()
把一个文件地址换成下载地址,结果按有效期缓存。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
url | string | 是 | 文件地址或缩略图地址 |
返回值:Promise<{ download_url: string; expires_at: string }>。
可能的错误:file_expired、file_blocked、not_found、not_group_member,以及网络错误。
im.files.resolveUrls()
把多个文件地址换成下载地址,每项单独给出结果。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
urls | string[] | 是 | 文件地址 |
返回值:Promise<ResolvedUrl[]>,按请求的顺序。
im.files.fetchBlob()
取得文件的内容。文件地址先换取下载地址,其他地址直接请求。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
url | string | 是 | 地址 |
p.onProgress | (loaded: number, total: number) => void | 否 | 下载进度;服务端没有给出大小时 total 为 0 |
p.signal | AbortSignal | 否 | 用于取消 |
返回值:Promise<Blob>。
可能的错误:换取下载地址的错误;not_found(下载地址返回 404)、download_failed、network_error、aborted。
im.files.groupFiles()
查询群文件列表,按上传时间从新到旧。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
group_id | string | 是 | 群 ID |
page.cursor | string | null | 否 | 下一页的游标 |
page.limit | number | 否 | 每页条数,默认 20,最大 100 |
返回值:Promise<Page<GroupFile> & { file_count: number; used_bytes: number }>。file_count 和 used_bytes 为这个群的群文件总数和占用的字节数。
可能的错误:not_found、not_group_member。
im.files.deleteGroupFile()
删除一个群文件:上传者本人、群主或管理员。已删除的同样成功。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
group_id | string | 是 | 群 ID |
file_id | string | 是 | 文件 ID |
返回值:Promise<void>。
可能的错误:permission_denied(not_uploader:不是上传者,也不是群主或管理员)、not_found、not_group_member、file_blocked(被屏蔽的文件不能删除)。
im.files.prepare()
按上传前的规则处理文件,不上传。不需要登录。
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
file | Blob | 是 | 文件 |
kind | FileKind | 是 | 希望的类型 |
p.original | boolean | 否 | 图片不压缩,只去掉元数据 |
返回值:Promise<PreparedFile>。
可能的错误:local_validation(file 不是 Blob)。
im.files.recordVoice()
创建录音器。
返回值:Promise<VoiceRecorder>。
可能的错误:unsupported(浏览器不支持录音)。
VoiceRecorder.start()
请求麦克风并开始录音。
返回值:Promise<void>。
可能的错误:permission_required(用户没有允许使用麦克风)、device_error(没有麦克风或被占用)。
VoiceRecorder.stop()
结束录音,释放麦克风。
返回值:Promise<{ blob: Blob; duration_seconds: number }>。
可能的错误:invalid_state(details.reason 为 not_recording:没有在录音)。
VoiceRecorder.cancel()
放弃录音,释放麦克风。同步执行。
数据结构
FileInfo
上传完成后的文件信息。
| 字段 | 类型 | 说明 |
|---|---|---|
file_id | string | 文件 ID |
purpose | string | 用途 |
kind | FileKind | 类型 |
status | string | 状态,上传完成后为 active |
url | string | 消息附件和群文件为文件地址,头像为公开地址 |
thumbnail_url | string | null | 缩略图地址,只有图片类型的消息附件和群文件有 |
name | string | null | 文件名 |
content_type | string | 服务端按内容识别出的格式,如 image/jpeg |
size | number | 大小,字节 |
width | number | null | 图片和头像的宽度,像素 |
height | number | null | 图片和头像的高度,像素 |
owner | string | null | 上传者的用户名 |
group_id | string | null | 群文件所在的群 ID |
via | string | 上传方式,客户端上传的为 client |
created_at | string | 上传时间,保留期从这时算起 |
expires_at | string | null | 过期时间:消息附件和设置了保留天数的群文件有值 |
GroupFile
群文件,在 FileInfo 的字段之外另有:
| 字段 | 类型 | 说明 |
|---|---|---|
owner_nickname | string | 上传者的昵称 |
owner_avatar_url | string | 上传者的头像 |
UploadParams
upload() 的第二个参数,字段见接口参考中的 im.files.upload()。
UploadTask
Promise<FileInfo>,另有:
| 方法 | 说明 |
|---|---|
cancel() | 取消上传,任务以 aborted 拒绝 |
FileKind
'image' | 'voice' | 'video' | 'file'。
PreparedFile
| 字段 | 类型 | 说明 |
|---|---|---|
blob | Blob | 处理后要上传的内容 |
kind | FileKind | 建议的类型;不能按请求的类型处理时为 file |
name | string | 处理后的文件名;图片重新编码后扩展名随之改变 |
format | string | 小写的格式名,如 jpeg、png、mp4;file 为扩展名 |
content_type | string | MIME 类型 |
width | number | 图片、视频的宽度,读不出时没有这一项 |
height | number | 图片、视频的高度 |
duration_seconds | number | 视频的时长,秒 |
thumbnail | Blob | 视频第一帧的 JPEG 图片 |
downgraded | string | kind 改为 file 的原因:image_too_large 或 unsupported_type |
ResolvedUrl
| 字段 | 类型 | 说明 |
|---|---|---|
url | string | 请求的文件地址 |
download_url | string | 下载地址,失败时没有这一项 |
expires_at | string | 下载地址的到期时间 |
error | DRError | 失败时的错误,见文件地址与下载地址 |
VoiceRecorder
| 成员 | 类型 | 说明 |
|---|---|---|
format | 'm4a' | 'aac' | 'ogg' | 'webm' | 录音的格式,创建时就已确定 |
start() | () => Promise<void> | 开始录音 |
stop() | () => Promise<{ blob: Blob; duration_seconds: number }> | 结束录音 |
cancel() | () => void | 放弃录音 |
