@yaebal/files
inspect, link, stream and download telegram files. adds ctx.files with three entry points — info, url and a lazy download() handle with Response-style readers — plus createFiles(api) for use outside middleware. understands self-hosted Bot API servers (--local).
install
pnpm add @yaebal/filesregistration
call bot.install(files()) once. the plugin adds ctx.files to every subsequent handler in the chain. the control is built once per api client — per-update cost is a single property.
import { Bot } from "@yaebal/core";
import { files } from "@yaebal/files";
const bot = new Bot(token);
bot.install(files());usage
info / url / download accept a file_id string, any api object with a file_id (Document, Audio, Voice, Sticker, …) or a PhotoSize[] array — the largest size wins.
bot.on("message:photo", async (ctx) => {
// ctx.photo is a size array — the largest size is picked automatically
const info = await ctx.files.info(ctx.photo); // getFile metadata, no bytes
if ((info.file_size ?? 0) > 10 * 1024 * 1024) return ctx.reply("too big");
await ctx.files.download(ctx.photo).toFile("./last-photo.jpg");
});the download handle
download() returns a lazy FileDownload: nothing is fetched until you read from it. info() costs one getFile (memoized); the body readers additionally fetch the bytes. like a Response, the body is single-use — read it once, call download() again for another pass. info(), url() and disk-sourced toFile() don't consume it.
const dl = ctx.files.download(ctx.document);
await dl.info(); // File metadata (one getFile, memoized)
await dl.url(); // download URL — ⚠️ embeds the bot token by default
await dl.bytes(); // Uint8Array
await dl.text(); // utf-8 string
await dl.json<Config>(); // parsed JSON
await dl.blob(); // Blob
await dl.stream(); // ReadableStream — pipe huge files, no buffering
await dl.toFile("./a.pdf"); // save to disk, returns the path
const bytes = await dl; // PromiseLike — awaiting yields Uint8Arrayoutside middleware
import { createFiles } from "@yaebal/files";
const files = createFiles(bot.api);
// onStart, cron jobs, workers, scripts — no ctx required
await files.download(fileId).toFile("./backup.bin");self-hosted bot api server
with --local the server reports absolute disk paths instead of CDN paths. strategy is picked per file (source: "auto"): relative path → classic URL; absolute path → baseUrl rewrite when configured, else a direct disk read (copy-on-disk toFile, zero transfer). force one with source: "url" | "disk" | "rewrite", or pass a function (file) => Promise<Uint8Array>.
bot.install(
files({
local: {
dir: "/var/lib/telegram-bot-api", // the server's working dir (default)
mount: "/data", // where that volume is mounted for the bot
baseUrl: "https://files.my.app", // serve the dir over HTTP → token-less url()
},
}),
);api
| export | kind | description |
|---|---|---|
files | function | (options?: FilesOptions) => Plugin — returns the plugin. |
createFiles | function | (api, options?) => FilesControl — the same control without middleware. |
FilesControl | interface | the type of ctx.files: info / url / download. |
FileDownload | class | the lazy handle: info, url, arrayBuffer, bytes, blob, text, json, stream, toFile; PromiseLike<Uint8Array>. |
FilesError | class | every failure, with a machine-readable reason: "bad-input", "no-file-path", "download-failed" (carries status), "no-url", "no-filesystem", "config". |
resolveFileId | function | collapse a FileInput (string / object / size array) to its file_id. |
FileInput, FilesOptions, FileSource, FileCallOptions | types | inputs, plugin options, strategy union, per-call options (signal). |
options
| option | default | what it does |
|---|---|---|
source | "auto" | byte-fetching strategy: "auto" / "url" / "disk" / "rewrite" / custom function. |
local.dir | /var/lib/telegram-bot-api | server working dir — the prefix of absolute file_paths it reports. |
local.mount | = dir | bot-side mount point of the shared volume (dir → mount remap before disk reads). |
local.baseUrl | — | public URL of the working dir — enables token-less links and "rewrite". |
fetch | globalThis.fetch | fetch override for byte downloads (proxy, instrumentation, tests). |
per call: ctx.files.download(file, { signal }) — the AbortSignal reaches both getFile and the byte fetch.
production notes
- the classic download URL embeds the bot token — don't show it to users or log it. configure
local.baseUrlfor token-less links. - the hosted Bot API caps
getFiledownloads at 20 MB; a local server lifts that to 2 GB. - byte downloads go straight through
fetch, not throughapihooks — @yaebal/again retries thegetFilecall, not the transfer itself. - prefer
stream()/toFile()overbytes()for large files — the buffered readers hold the whole file in memory.
testing
stub getFile with @yaebal/test; inject fetch for the byte transfer.
const env = createTestEnv(bot);
env.onApi("getFile", { file_id: "DOC", file_unique_id: "U", file_path: "documents/d.pdf" });
// byte downloads are injectable too:
bot.install(files({ fetch: myFakeFetch }));related
- media & files guide — uploading with the core
media.*sources. - @yaebal/file-id — look inside a
file_id: datacenter, access hash, dedupe key. - @yaebal/media-cache — reuse a
file_idinstead of re-uploading.