From 2e3cf7850c40d19260a01e1340517ee4e7334077 Mon Sep 17 00:00:00 2001 From: silencoo <199365119+silencoo@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:25:40 +0900 Subject: [PATCH] Add original-quality image uploads --- README-zh.md | 8 +++- README.md | 8 +++- functions/storage/index.js | 2 +- functions/storage/telegram.js | 4 +- functions/upload.js | 29 ++++++++++++- functions/utils/telegram.js | 5 ++- index.html | 38 +++++++++++++++-- test/upload.test.js | 78 ++++++++++++++++++++++++++++++++++- 8 files changed, 161 insertions(+), 11 deletions(-) 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 @@
点击、拖拽或粘贴上传支持批量上传
图片 / 视频 / 音频 / 文档