diff --git a/README-zh.md b/README-zh.md index ead8f0912..76a78ba8b 100644 --- a/README-zh.md +++ b/README-zh.md @@ -119,7 +119,7 @@ 8.可插拔的存储后端:文件默认存储在 Telegram,也可通过一个环境变量切换到 Cloudflare R2 存储桶——切换后旧链接依然有效 -9.批量上传,支持拖拽和粘贴上传、逐文件进度显示,以及 URL / Markdown / BBCode / HTML 四种格式一键复制;可选的 Referer 白名单防盗链 +9.批量上传,支持拖拽和粘贴上传、图片原画质上传、逐文件进度显示,以及 URL / Markdown / BBCode / HTML 四种格式一键复制;可选的 Referer 白名单防盗链 10.部署自检:配置缺失时首页会直接指出缺哪个环境变量或绑定、去哪里补,而不是等到第一次上传才失败 @@ -211,6 +211,12 @@ curl -F "file=@/path/to/image.png" https://your.domain/upload ``` +使用 Telegram 存储时,如需保留图片原画质,可增加 `imageUploadMode=document` 将图片作为文件发送。为了保证生成的链接仍能通过 Bot API 加载,原画质图片最大支持 20MB: + +```bash +curl -F "file=@/path/to/image.png" -F "imageUploadMode=document" https://your.domain/upload +``` + 返回一个 JSON 数组,`src` 为文件的访问路径(开启短链接功能后,这里会直接返回短链接): ```json diff --git a/README.md b/README.md index 74d9b2c2d..80c0313d7 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ Bindings (`Settings` -> `Functions`): 8. Pluggable storage: files live on Telegram by default, or in a Cloudflare R2 bucket with one environment variable — old links keep working either way -9. Batch upload with drag & drop and paste support, per-file progress, and one-click copy as URL / Markdown / BBCode / HTML; optional anti-hotlinking via a referer allowlist +9. Batch upload with drag & drop and paste support, optional original-quality image uploads, per-file progress, and one-click copy as URL / Markdown / BBCode / HTML; optional anti-hotlinking via a referer allowlist 10. Deployment self-check: when configuration is incomplete the homepage says which environment variable or binding is missing and where to set it, instead of failing on the first upload @@ -211,6 +211,12 @@ The upload endpoint is `POST /upload` using `multipart/form-data`, with the file curl -F "file=@/path/to/image.png" https://your.domain/upload ``` +To preserve an image's original quality with Telegram storage, send it as a document by adding `imageUploadMode=document`. Original-quality Telegram images are limited to 20MB so the resulting file can still be served through the Bot API: + +```bash +curl -F "file=@/path/to/image.png" -F "imageUploadMode=document" https://your.domain/upload +``` + The response is a JSON array where `src` is the file's access path (with short links enabled, this returns the short link directly): ```json diff --git a/functions/storage/index.js b/functions/storage/index.js index 924ebc3ad..8978fb912 100644 --- a/functions/storage/index.js +++ b/functions/storage/index.js @@ -4,7 +4,7 @@ import { r2Provider } from './r2.js'; // Storage provider contract: // key - tag persisted in KV metadata for provenance // validateConfig(env) - throws when required bindings/vars are missing -// upload(env, file, { fileName, fileExtension }) -> long file id (string) +// upload(env, file, { fileName, fileExtension, imageUploadMode }) -> long file id (string) // fetchFile(env, request, url, fileId) -> Response with the file body const PROVIDERS = { [telegramProvider.key]: telegramProvider, diff --git a/functions/storage/telegram.js b/functions/storage/telegram.js index 6c5af4d26..81f07db20 100644 --- a/functions/storage/telegram.js +++ b/functions/storage/telegram.js @@ -14,8 +14,8 @@ export const telegramProvider = { validateTelegramConfig(env); }, - async upload(env, file, { fileExtension }) { - const { endpoint, field } = getUploadTarget(file); + async upload(env, file, { fileExtension, imageUploadMode }) { + const { endpoint, field } = getUploadTarget(file, imageUploadMode); const formData = createTelegramFormData(env.TG_Chat_ID, field, file); const result = await sendToTelegram(formData, endpoint, env); diff --git a/functions/upload.js b/functions/upload.js index fb3a95cee..fe74526a5 100644 --- a/functions/upload.js +++ b/functions/upload.js @@ -5,6 +5,8 @@ import { createDefaultMetadata, putMetadata } from "./utils/metadata.js"; import { allocateShortId, isShortUrlsEnabled, putShortLink } from "./utils/shortlink.js"; import { getUploadProvider } from "./storage/index.js"; +const TELEGRAM_SERVABLE_FILE_LIMIT = 20 * 1024 * 1024; + export async function onRequestPost(context) { const { request, env } = context; @@ -28,10 +30,35 @@ export async function onRequestPost(context) { throw new Error('No file uploaded'); } + const requestedImageUploadMode = formData.get('imageUploadMode'); + if ( + requestedImageUploadMode !== null + && requestedImageUploadMode !== 'photo' + && requestedImageUploadMode !== 'document' + ) { + return jsonResponse({ error: 'Invalid image upload mode' }, { status: 400 }); + } + const imageUploadMode = requestedImageUploadMode || 'photo'; + + if ( + provider.key === 'telegram' + && imageUploadMode === 'document' + && uploadFile.type.startsWith('image/') + && uploadFile.size > TELEGRAM_SERVABLE_FILE_LIMIT + ) { + return jsonResponse({ + error: 'Original-quality Telegram images must not exceed 20 MB', + }, { status: 413 }); + } + const fileName = uploadFile.name; const fileExtension = fileName.split('.').pop().toLowerCase(); - const longId = await provider.upload(env, uploadFile, { fileName, fileExtension }); + const longId = await provider.upload(env, uploadFile, { + fileName, + fileExtension, + imageUploadMode, + }); let shortId = null; // 将文件信息保存到 KV 存储 diff --git a/functions/utils/telegram.js b/functions/utils/telegram.js index c1126c498..d035053ab 100644 --- a/functions/utils/telegram.js +++ b/functions/utils/telegram.js @@ -12,8 +12,11 @@ export function validateTelegramConfig(env) { } } -export function getUploadTarget(file) { +export function getUploadTarget(file, imageUploadMode = 'photo') { if (file.type.startsWith('image/')) { + if (imageUploadMode === 'document') { + return { endpoint: 'sendDocument', field: 'document' }; + } return { endpoint: 'sendPhoto', field: 'photo' }; } diff --git a/index.html b/index.html index 75b33a192..2967dad69 100644 --- a/index.html +++ b/index.html @@ -143,6 +143,16 @@ .btn.small { padding: 7px 14px; font-size: 12px; gap: 6px; } .btn .icon { width: 13px; height: 13px; } #file-input { display: none; } +.quality-option { + display: flex; align-items: center; gap: 8px; width: 100%; max-width: 220px; + padding: 8px 10px; border: 1px solid var(--line); border-radius: 7px; + background: rgba(255, 255, 255, .72); cursor: pointer; +} +.quality-option:hover { border-color: #dce3e9; background: #fff; } +.quality-option[hidden] { display: none; } +.quality-option input { width: 15px; height: 15px; margin: 0; accent-color: var(--accent); flex: none; } +.quality-option span { min-width: 0; font-size: 11px; line-height: 1.45; color: var(--muted); } +.quality-option b { display: block; color: var(--ink); font-size: 12px; } /* ---- right: results ---- */ .results { flex: 1; min-width: 0; display: flex; flex-direction: column; } @@ -285,6 +295,10 @@

Telegraph-Image

点击、拖拽或粘贴上传支持批量上传
图片 / 视频 / 音频 / 文档

+ @@ -328,6 +342,7 @@

ORIGINAL_QUALITY_MAX_SIZE + ) { + failItem(item, file, '原画质模式最大支持 20 MB'); + return; + } + pending.push({ file: file, item: item, imageUploadMode: imageUploadMode }); }); updateCount(); pumpQueue(); @@ -467,7 +498,7 @@

{ + assert.strictEqual(String(input), 'https://api.telegram.org/botbot-token/sendDocument'); + assert.ok(init.body.get('document') instanceof File); + assert.strictEqual(init.body.get('photo'), null); + return Response.json({ + ok: true, + result: { document: { file_id: 'original-id' } }, + }); + }); + + const request = await createUploadRequest( + new File(['image-bytes'], 'cat.png', { type: 'image/png' }), + 'document', + ); + const res = await onRequestPost(makeContext({ + request, + env: { + disable_telemetry: 'true', + TG_Bot_Token: 'bot-token', + TG_Chat_ID: '-100123', + }, + })); + + assert.strictEqual(res.status, 200); + assert.deepStrictEqual(JSON.parse(await res.text()), [{ src: '/file/original-id.png' }]); + assert.strictEqual(fetchMock.calls.length, 1); + }); + + it('rejects original-quality Telegram images larger than the 20 MB serving limit', async function () { + const { onRequestPost } = await import('../functions/upload.js'); + const oversizedImage = new File( + [new Uint8Array(20 * 1024 * 1024 + 1)], + 'large.png', + { type: 'image/png' }, + ); + const request = await createUploadRequest(oversizedImage, 'document'); + + const res = await onRequestPost(makeContext({ + request, + env: { + disable_telemetry: 'true', + TG_Bot_Token: 'bot-token', + TG_Chat_ID: '-100123', + }, + })); + + assert.strictEqual(res.status, 413); + assert.deepStrictEqual(JSON.parse(await res.text()), { + error: 'Original-quality Telegram images must not exceed 20 MB', + }); + }); + + it('rejects an unsupported image upload mode', async function () { + const { onRequestPost } = await import('../functions/upload.js'); + const request = await createUploadRequest( + new File(['image-bytes'], 'cat.png', { type: 'image/png' }), + 'lossless-ish', + ); + + const res = await onRequestPost(makeContext({ + request, + env: { + disable_telemetry: 'true', + TG_Bot_Token: 'bot-token', + TG_Chat_ID: '-100123', + }, + })); + + assert.strictEqual(res.status, 400); + assert.deepStrictEqual(JSON.parse(await res.text()), { error: 'Invalid image upload mode' }); + }); + it('uploads non-media files with sendDocument', async function () { const { onRequestPost } = await import('../functions/upload.js');