分享文件使用网盘一直是一种标准解决方案,但实际使用时动不动就内容审核被删了、下载限速、登录后使用的体验还是不算友好。针对比较小的文件分享还是想要有一个比较持久的解决方案,考虑了 Cloudflare Workers + R2、GitHub Repositorie 等形式,最终决定使用 Telegram Bot 实现。
选择 Telegram Bot 主要是因为文件存储长久有效、上传过程就是文件发送,算是最简单快捷的处理方式了。但是劣势也很明显,文件大小受限、文件管理困难、受限于访问环境等。
架构与技术栈
- 存储服务: Telegram
- 运行环境:
- Telegram Bot 作为文件上传入口接收分享链接返回信息。
- Cloudflare Workers 处理 Bot 逻辑以及外链路由。
- 编程语言: Javascript
采用 Worker Serverless 实现,业务逻辑分为上传、下载两部分:
- 上传: 通过 Telegram Bot 发送文件,Cloudflare Workers 调用 Telegram API 生成外部使用的分享链接。
- 下载: Cloudflare Workers 通过分享链接重定向到 Telegram API 文件下载链接实现文件下载。
部署步骤
- 在 Telegram 中的 BotFather 中创建 Bot,并获取 HTTP API Token。
- 在 Cloudflare Workers 中创建 Worker 并使用下方代码部署。
javascript
export default {
async fetch(request, env) {
const url = new URL(request.url)
const origin = request.headers.get('origin') || `https://${url.hostname}`
const MAX_FILE_SIZE = 0 //20 * 1024 * 1024; // 20MB
// 初始化 Telegram Webhook
if (url.pathname === '/setWebhook') {
const webhookUrl = `${origin}/webhook`
const telegramApiUrl = `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/setWebhook`
const response = await fetch(telegramApiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: webhookUrl }),
})
const result = await response.json()
if (!result.ok) {
return new Response(JSON.stringify({ error: 'Failed to set webhook', details: result }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
})
}
return new Response(JSON.stringify({ message: 'Webhook set successfully', webhookUrl }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}
// 处理 Telegram Webhook 回调
if (url.pathname === '/webhook' && request.method === 'POST') {
const update = await request.json()
const message = update.message
if (message) {
let fileId, fileType, fileSize
if (message.document) {
fileId = message.document.file_id
fileType = 'document'
fileSize = message.document.file_size
} else if (message.photo) {
const largestPhoto = message.photo[message.photo.length - 1]
fileId = largestPhoto.file_id
fileType = 'photo'
fileSize = largestPhoto.file_size
} else if (message.video) {
fileId = message.video.file_id
fileType = 'video'
fileSize = message.video.file_size
} else if (message.audio) {
fileId = message.audio.file_id
fileType = 'audio'
fileSize = message.audio.file_size
} else if (message.voice) {
fileId = message.voice.file_id
fileType = 'voice'
fileSize = message.voice.file_size
} else if (message.animation) {
fileId = message.animation.file_id
fileType = 'animation'
fileSize = message.animation.file_size
} else {
return new Response('Unsupported file type', { status: 200 })
}
// 检查文件大小限制
if (MAX_FILE_SIZE > 0 && fileSize > MAX_FILE_SIZE) {
const telegramApiUrl = `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendMessage`
const payload = {
chat_id: message.chat.id,
text: `The uploaded ${fileType} exceeds the ${MAX_FILE_SIZE / 1024 / 1024}MB limit. Please upload a smaller file.`,
}
await fetch(telegramApiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
return new Response('File too large', { status: 200 })
}
// 生成访问链接
const accessLink = `${origin}/file?id=${encodeURIComponent(fileId)}`
// 回复
const telegramApiUrl = `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendMessage`
const payload = {
chat_id: message.chat.id,
text: `Your ${fileType} has been uploaded successfully: ${accessLink}`,
}
await fetch(telegramApiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
return new Response('Webhook processed', { status: 200 })
}
return new Response('No file found in the update', { status: 200 })
}
// 文件访问处理
if (url.pathname === '/file') {
const fileId = url.searchParams.get('id')
if (!fileId) {
return new Response('Missing file ID', { status: 400 })
}
const fileInfoApi = `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/getFile?file_id=${fileId}`
const fileInfoResponse = await fetch(fileInfoApi)
const fileInfoResult = await fileInfoResponse.json()
if (!fileInfoResult.ok) {
return new Response('Failed to retrieve file info', { status: 500 })
}
const filePath = fileInfoResult.result.file_path
const fileUrl = `https://api.telegram.org/file/bot${env.TELEGRAM_BOT_TOKEN}/${filePath}`
return Response.redirect(fileUrl, 302)
}
// 其他未匹配的路由
return new Response('Not Found', { status: 404 })
},
}
- Cloudflare Workers 设置中绑定自定义域名,并将第一步得到的 Token 存储到
TELEGRAM_BOT_TOKEN
变量中。
- 访问
https://example.com/setWebhook
完成初始化,注意将域名换为第三步中绑定的自定义域名。
至此,就可以尝试在 Bot 中发送文件获取外部访问链接。
实际体验
“理想很丰满,现实很骨感”
本想实现一个不受文件大小、数量限制的方案,但实际上,外部访问链接只支持 5 MB 之内的图片文件、20 MB 之内的其他文件。大大限制了可应用的范围,但是作为网站的图床或者分享其他小媒体资源分享应该是足够了。