mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-20 20:43:21 +08:00
feat: support inbound files across all channels
This commit is contained in:
parent
29bcb67a64
commit
0a26d7df92
45 changed files with 2651 additions and 343 deletions
64
src/channels/shared/file-download.mjs
Normal file
64
src/channels/shared/file-download.mjs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
async function cancelResponseBody(response) {
|
||||
try {
|
||||
await response?.body?.cancel?.();
|
||||
} catch {
|
||||
// Preserve the original download failure.
|
||||
}
|
||||
}
|
||||
|
||||
function hostedByMessagingPlatform(target, allowedHosts) {
|
||||
return !Array.isArray(allowedHosts) || allowedHosts.some((rule) => (
|
||||
typeof rule === 'string'
|
||||
&& (target.hostname === rule
|
||||
|| (rule.startsWith('.')
|
||||
&& (target.hostname === rule.slice(1) || target.hostname.endsWith(rule))))
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a channel-hosted ordinary file as a stream.
|
||||
*
|
||||
* This deliberately has no plugin-defined size, type, count, or download-time
|
||||
* limit. The caller owns cancellation through its AbortSignal and the channel
|
||||
* remains the authority for its own file limits.
|
||||
*/
|
||||
export async function fetchFileStream(url, {
|
||||
fetchImpl = fetch,
|
||||
headers,
|
||||
signal,
|
||||
allowedHosts,
|
||||
} = {}) {
|
||||
const target = new URL(url);
|
||||
if (target.protocol !== 'https:') throw new Error('File download URL must use HTTPS');
|
||||
if (!hostedByMessagingPlatform(target, allowedHosts)) {
|
||||
throw new Error('File download URL is not hosted by the messaging platform');
|
||||
}
|
||||
|
||||
const response = await fetchImpl(target, {
|
||||
method: 'GET',
|
||||
headers,
|
||||
signal,
|
||||
redirect: 'manual',
|
||||
});
|
||||
if (Number.isInteger(response?.status) && response.status >= 300 && response.status < 400) {
|
||||
await cancelResponseBody(response);
|
||||
const error = new Error(`File download redirect was blocked (HTTP ${response.status})`);
|
||||
error.code = 'file-redirect-blocked';
|
||||
throw error;
|
||||
}
|
||||
if (!response?.ok) {
|
||||
await cancelResponseBody(response);
|
||||
const error = new Error(`File download failed with HTTP ${response?.status ?? 'unknown'}`);
|
||||
error.code = 'file-http-error';
|
||||
error.status = response?.status;
|
||||
throw error;
|
||||
}
|
||||
if (response.body?.[Symbol.asyncIterator]) return { stream: response.body };
|
||||
if (typeof response.arrayBuffer === 'function') {
|
||||
const data = Buffer.from(await response.arrayBuffer());
|
||||
return {
|
||||
stream: (async function* fileBody() { yield data; }()),
|
||||
};
|
||||
}
|
||||
throw new Error('File download returned no readable body');
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue