檔案
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 | 放棄錄音 |
