├── API-测试功能.md ├── LICENSE ├── README.md ├── _worker.api.js ├── _worker.js └── html ├── admin.html ├── loader.js ├── login.html └── upload.html /API-测试功能.md: -------------------------------------------------------------------------------- 1 | # API 使用文档 2 | 此为测试功能,谨慎使用 3 | 4 | ## 认证方式 5 | 6 | 所有API请求需携带API Key,可通过以下方式传递: 7 | 8 | |方式|示例| 9 | |---|---| 10 | |请求头|`X-API-Key: your_api_key`| 11 | |URL参数|`/api/files?api_key=your_api_key`| 12 | 13 | ## API端点速查表 14 | 15 | ### 文件上传 16 | 17 | |方法|路径|参数|成功响应| 18 | |---|---|---|---| 19 | |`POST`|`/api/upload`|`file` (文件表单字段)|`{url, file_name, file_size, mime_type}`| 20 | 21 | ### 文件管理 22 | 23 | |方法|路径|说明|成功响应| 24 | |---|---|---|---| 25 | |`GET`|`/api/files`|获取文件列表|`{files: [...]}`| 26 | |`GET`|`/api/files/{path}`|获取单个文件信息|文件信息对象| 27 | |`DELETE`|`/api/files/{path}`|删除指定文件|`{success: true}`| 28 | 29 | ### 文件搜索 30 | 31 | |方法|路径|参数|成功响应| 32 | |---|---|---|---| 33 | |`GET`|`/api/search`|`q` (搜索关键词)|`{files: [...]}`| 34 | 35 | ## 常用错误码 36 | 37 | |状态码|说明| 38 | |---|---| 39 | |400|请求参数错误/文件过大| 40 | |401|API Key无效| 41 | |404|文件不存在| 42 | |500|服务器内部错误| 43 | 44 | ## 快速示例 45 | 46 | ```bash 47 | # 上传文件 48 | curl -X POST -H "X-API-Key: xxx" -F "file=@test.jpg" https://domain.com/api/upload 49 | 50 | # 删除文件 51 | curl -X DELETE -H "X-API-Key: xxx" https://your-domain.com/api/files/文件名.jpg 52 | 53 | # 获取文件列表 54 | curl -H "X-API-Key: xxx" https://domain.com/api/files 55 | 56 | # 搜索文件 57 | curl -H "X-API-Key: xxx" https://domain.com/api/search?q=关键词 58 | ``` 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT 许可证(MIT License) 2 | 3 | 版权所有 (c) [2025] [yutian81] 4 | 5 | 特此授予任何获得本软件副本的人员,免费使用本软件, 6 | 包括但不限于使用、复制、修改、合并、发布、分发、再许可 7 | 及/或发布本软件的副本,条件是: 8 | 9 | 保留版权声明和许可证声明:上述版权声明以及本许可证声明 10 | 应包含在本软件的所有副本或重要部分中。 11 | 12 | 禁止商业用途:本软件的使用仅限于非商业目的,不得用于 13 | 任何形式的商业销售、转售或其他盈利目的。不得在未经授权的 14 | 情况下将本软件或其部分内容用于商业产品或服务中。 15 | 16 | 修改和再分发:如果您修改了本软件并进行再分发,您必须 17 | 明确标明所做的更改,并且必须附带此许可证声明。 18 | 19 | 无担保声明:本软件按“原样”提供,不附带任何形式的明示 20 | 或暗示的担保,包括但不限于对适销性、特定用途适用性 21 | 及非侵权的担保。在任何情况下,作者或版权持有者均不对 22 | 因使用本软件或与本软件有关的其他行为(包括但不限于修改、 23 | 分发、使用等)而导致的任何索赔、损害或其他责任负责, 24 | 无论是在合同、侵权或其他法律责任形式下。 25 | 26 | 责任限制:本软件的使用者应对其使用本软件所带来的任何后果 27 | 或风险自行负责。任何因使用本软件或依赖本软件的行为引起的 28 | 直接、间接、偶然、特殊、惩罚性或后续的损害赔偿,作者 29 | 和版权持有者不承担任何责任。 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TGFile 2 | 3 | ## 项目特点 4 | 该项目是一个基于 Cloudflare Worker 环境和 telegram 频道存储功能的文件上传、分享、下载、搜索、在线预览系统。支持用户认证,上传的文件将通过 Telegram 机器人发送到指定的频道聊天中。 5 | 6 | ## 功能 7 | - **用户认证**:可选是否开启身份认证,默认开启 `ENABLE_AUTH = true`,如设置为 `false`,则跳过登录。 8 | - **前端登录**:`ENABLE_AUTH = true`的情况下,需要输入用户名密码登录,`cookie`有效期为默认为7天。 9 | - **文件上传**:用户可以通过拖拽或点击选择文件进行上传,支持多文件上传,支持显示上传进度条百分比。 10 | - **文件管理**:管理员可以查看已上传的文件列表,支持在线预览(图片和视频格式)、分享、下载和删除文件。 11 | - 分享:会生成一个二维码,二维码框内点击“复制链接”按钮也可复制url链接 12 | - 下载:直接调用浏览器下载功能 13 | - 删除:会同步从tg频道中删除上传的文件 14 | - **文件搜索**:用户可以根据文件名搜索已上传的文件。 15 | - **背景图更新**:系统会定期从 Bing 获取背景图,提升用户体验。 16 | 17 | ## 2025-02-11 更新 18 | - 为前端页面增加图标和网站描述 19 | 20 | ## 2025-02-09 更新 21 | - 解决webp图片上传失败的问题 22 | - 文件管理页面删除文件时,可以同步从tg频道删除消息 23 | - 文件管理页面点击分享可生成二维码 24 | - **注意**:本次更新需要重写D1数据表,先删除在D1中生成的表文件,重新访问项目主页以生成新的表文件 25 | 26 | ## 部署方法 27 | 28 | 1. **粘贴代码**: 29 | - 新建一个 worker,复制 `_worker.js` 的内容粘贴到项目中并部署 30 | 31 | 2. **绑定数据库(必须)**: 32 | - 创建一个D1数据库,数据库名随意,例如:`tgflie` 33 | - 在worker项目的设置中绑定刚刚创建的数据库,变量名:`DATABASE` 34 | 35 | 3. **配置环境变量**: 36 | | 变量名 | 变量值 | 是否必须 | 备注 | 37 | | ----- | ------ | ------- | ---- | 38 | | DOMAIN | 你项目绑定的域名 | √ | | 39 | | USERNAME | 登录用户名 | × | | 40 | | PASSWORD | 登录密码 | × | | 41 | | COOKIE | 7 | × | cookie有效期,默认为7 | 42 | | ENABLE_AUTH | true / false | × | 默认为true,如果设为false,则不启用登录 | 43 | | TG_BOT_TOKEN | TG机器人 token | √ | 获取方式请自行谷歌,下同 | 44 | | TG_CHAT_ID | TG频道ID | √ | 是频道ID,不是机器人ID,格式为`-10*****062333` | 45 | | MAX_SIZE_MB | 1024 | × | 上传的单文件大小上限,默认为1g | 46 | 47 | ## 访问应用 48 | 打开浏览器,访问 `http://你绑定的域名`,首次登录会要求输入用户名和密码,然后进行文件上传和管理。二次登录会跳过登录环节,直接进入上传页面,cookie 有效期为 7 天,超过 7 天会要求重新登录。你也可以在环境变量中设置 COOKIE 变量改变有效期 49 | 50 | ## 已知问题 51 | - 由于TG的限制,20M以上的文件虽然可以上传,但无法返回直链,导致无法预览 52 | - Worker免费计划也有每个请求的响应时间不能超过10秒的限制 53 | - 因此修改代码逻辑:前端禁止上传超过20M的文件,超过会弹出错误信息 54 | 55 | ## Plan 计划 56 | - 增加更多文件格式的在线预览 57 | - 文件管理页面增加批量删除功能 58 | 59 | ## 鸣谢 60 | 感谢这位[大佬](https://github.com/0-RTT/telegraph)给予的灵感,有一些代码借鉴于此 61 | 62 | ## 贡献 63 | 欢迎任何形式的贡献!请提交问题或拉取请求。 64 | 65 | ## 许可证 66 | 该项目采用 MIT 许可证,详细信息请查看 LICENSE 文件。 67 | -------------------------------------------------------------------------------- /_worker.api.js: -------------------------------------------------------------------------------- 1 | // 由于tg的限制,虽然可以上传超过20M的文件,但无法返回直链地址 2 | // 因此修改代码,当文件大于20MB时,直接阻止上传 3 | 4 | import { loadTemplate, render } from './html/loader.js'; 5 | 6 | // 数据库初始化函数 7 | async function initDatabase(config) { 8 | await config.database.prepare(` 9 | CREATE TABLE IF NOT EXISTS files ( 10 | url TEXT PRIMARY KEY, 11 | fileId TEXT NOT NULL, 12 | message_id INTEGER NOT NULL, 13 | created_at INTEGER NOT NULL, 14 | file_name TEXT, 15 | file_size INTEGER, 16 | mime_type TEXT 17 | ) 18 | `).run(); 19 | } 20 | 21 | // 导出函数 22 | export default { 23 | async fetch(request, env) { 24 | // 环境变量配置 25 | const config = { 26 | domain: env.DOMAIN, 27 | database: env.DATABASE, 28 | username: env.USERNAME, 29 | password: env.PASSWORD, 30 | enableAuth: env.ENABLE_AUTH === 'true', 31 | tgBotToken: env.TG_BOT_TOKEN, 32 | tgChatId: env.TG_CHAT_ID, 33 | cookie: Number(env.COOKIE) || 7, // cookie有效期默认为 7 34 | maxSizeMB: Number(env.MAX_SIZE_MB) || 20, // 上传单文件大小默认为20M 35 | apiKey: env.API_KEY 36 | }; 37 | 38 | // 初始化数据库 39 | await initDatabase(config); 40 | // 路由处理 41 | const { pathname } = new URL(request.url); 42 | if (pathname === '/config') { 43 | const safeConfig = { maxSizeMB: config.maxSizeMB }; 44 | return new Response(JSON.stringify(safeConfig), { 45 | headers: { 'Content-Type': 'application/json' } 46 | }); 47 | } 48 | 49 | // 增加API路由 50 | const apiRoutes = { 51 | '/api/upload': () => handleApiUpload(request, config), 52 | '/api/files': () => handleApiFileList(request, config), 53 | '/api/files/([^/]+)$': () => handleApiFileOps(request, config), 54 | '/api/search': () => handleApiSearch(request, config) 55 | }; 56 | // 检查API请求 57 | for (const [path, handler] of Object.entries(apiRoutes)) { 58 | if (pathname.match(new RegExp(`^${path}$`))) { 59 | return await handler(); 60 | } 61 | } 62 | 63 | const routes = { 64 | '/': () => handleAuthRequest(request, config), 65 | '/login': () => handleLoginRequest(request, config), 66 | '/upload': () => handleUploadRequest(request, config), 67 | '/admin': () => handleAdminRequest(request, config), 68 | '/delete': () => handleDeleteRequest(request, config), 69 | '/search': () => handleSearchRequest(request, config), 70 | '/bing': handleBingImagesRequest 71 | }; 72 | const handler = routes[pathname]; 73 | if (handler) { 74 | return await handler(); 75 | } 76 | // 处理文件访问请求 77 | return await handleFileRequest(request, config); 78 | } 79 | }; 80 | 81 | // API认证中间件 82 | async function authenticateApi(request, config) { 83 | const apiKey = request.headers.get('X-API-Key') || 84 | new URL(request.url).searchParams.get('api_key'); 85 | 86 | if (!apiKey || apiKey !== env.API_KEY) { 87 | return new Response(JSON.stringify({ error: 'Invalid API key' }), { 88 | status: 401, 89 | headers: { 'Content-Type': 'application/json' } 90 | }); 91 | } 92 | return null; // 认证通过 93 | } 94 | 95 | // API文件上传处理 96 | async function handleApiUpload(request, config) { 97 | const authError = await authenticateApi(request, config); 98 | if (authError) return authError; 99 | 100 | if (request.method !== 'POST') { 101 | return new Response(JSON.stringify({ error: 'Method not allowed' }), { 102 | status: 405, 103 | headers: { 'Content-Type': 'application/json' } 104 | }); 105 | } 106 | 107 | try { 108 | const formData = await request.formData(); 109 | const file = formData.get('file'); 110 | 111 | if (!file) { 112 | return new Response(JSON.stringify({ error: 'No file provided' }), { 113 | status: 400, 114 | headers: { 'Content-Type': 'application/json' } 115 | }); 116 | } 117 | 118 | // 重用现有的上传逻辑 119 | const uploadResponse = await handleUploadRequest(new Request(request.url, { 120 | method: 'POST', 121 | body: formData 122 | }), config); 123 | 124 | const data = await uploadResponse.json(); 125 | 126 | if (uploadResponse.status !== 200) { 127 | return new Response(JSON.stringify(data), { 128 | status: uploadResponse.status, 129 | headers: { 'Content-Type': 'application/json' } 130 | }); 131 | } 132 | 133 | return new Response(JSON.stringify({ 134 | url: data.url, 135 | file_name: file.name, 136 | file_size: file.size, 137 | mime_type: file.type 138 | }), { 139 | headers: { 'Content-Type': 'application/json' } 140 | }); 141 | 142 | } catch (error) { 143 | return new Response(JSON.stringify({ error: error.message }), { 144 | status: 500, 145 | headers: { 'Content-Type': 'application/json' } 146 | }); 147 | } 148 | } 149 | 150 | // API文件列表处理 151 | async function handleApiFileList(request, config) { 152 | const authError = await authenticateApi(request, config); 153 | if (authError) return authError; 154 | 155 | if (request.method !== 'GET') { 156 | return new Response(JSON.stringify({ error: 'Method not allowed' }), { 157 | status: 405, 158 | headers: { 'Content-Type': 'application/json' } 159 | }); 160 | } 161 | 162 | try { 163 | const { limit = 50, offset = 0 } = Object.fromEntries(new URL(request.url).searchParams); 164 | 165 | const files = await config.database.prepare( 166 | `SELECT url, file_name, file_size, mime_type, created_at 167 | FROM files 168 | ORDER BY created_at DESC 169 | LIMIT ? OFFSET ?` 170 | ).bind(limit, offset).all(); 171 | 172 | return new Response(JSON.stringify({ files: files.results || [] }), { 173 | headers: { 'Content-Type': 'application/json' } 174 | }); 175 | 176 | } catch (error) { 177 | return new Response(JSON.stringify({ error: error.message }), { 178 | status: 500, 179 | headers: { 'Content-Type': 'application/json' } 180 | }); 181 | } 182 | } 183 | 184 | // API文件操作处理 185 | async function handleApiFileOps(request, config) { 186 | const authError = await authenticateApi(request, config); 187 | if (authError) return authError; 188 | 189 | const url = new URL(request.url); 190 | const fileUrl = `https://${config.domain}${url.pathname.replace('/api/files', '')}`; 191 | 192 | if (request.method === 'GET') { 193 | // 获取文件信息 194 | const file = await config.database.prepare( 195 | `SELECT url, file_name, file_size, mime_type, created_at 196 | FROM files WHERE url = ?` 197 | ).bind(fileUrl).first(); 198 | 199 | if (!file) { 200 | return new Response(JSON.stringify({ error: 'File not found' }), { 201 | status: 404, 202 | headers: { 'Content-Type': 'application/json' } 203 | }); 204 | } 205 | 206 | return new Response(JSON.stringify(file), { 207 | headers: { 'Content-Type': 'application/json' } 208 | }); 209 | 210 | } else if (request.method === 'DELETE') { 211 | // 删除文件 212 | const deleteResponse = await handleDeleteRequest(new Request(request.url, { 213 | method: 'POST', 214 | headers: { 'Content-Type': 'application/json' }, 215 | body: JSON.stringify({ url: fileUrl }) 216 | }), config); 217 | 218 | const data = await deleteResponse.json(); 219 | 220 | return new Response(JSON.stringify(data), { 221 | status: deleteResponse.status, 222 | headers: { 'Content-Type': 'application/json' } 223 | }); 224 | 225 | } else { 226 | return new Response(JSON.stringify({ error: 'Method not allowed' }), { 227 | status: 405, 228 | headers: { 'Content-Type': 'application/json' } 229 | }); 230 | } 231 | } 232 | 233 | // API搜索处理 234 | async function handleApiSearch(request, config) { 235 | const authError = await authenticateApi(request, config); 236 | if (authError) return authError; 237 | 238 | if (request.method !== 'GET') { 239 | return new Response(JSON.stringify({ error: 'Method not allowed' }), { 240 | status: 405, 241 | headers: { 'Content-Type': 'application/json' } 242 | }); 243 | } 244 | 245 | try { 246 | const { q } = Object.fromEntries(new URL(request.url).searchParams); 247 | 248 | if (!q) { 249 | return new Response(JSON.stringify({ error: 'Missing search query' }), { 250 | status: 400, 251 | headers: { 'Content-Type': 'application/json' } 252 | }); 253 | } 254 | 255 | const searchPattern = `%${q}%`; 256 | const files = await config.database.prepare( 257 | `SELECT url, file_name, file_size, mime_type, created_at 258 | FROM files 259 | WHERE file_name LIKE ? ESCAPE '!' 260 | COLLATE NOCASE 261 | ORDER BY created_at DESC` 262 | ).bind(searchPattern).all(); 263 | 264 | return new Response(JSON.stringify({ files: files.results || [] }), { 265 | headers: { 'Content-Type': 'application/json' } 266 | }); 267 | 268 | } catch (error) { 269 | return new Response(JSON.stringify({ error: error.message }), { 270 | status: 500, 271 | headers: { 'Content-Type': 'application/json' } 272 | }); 273 | } 274 | } 275 | 276 | // 处理身份认证 277 | function authenticate(request, config) { 278 | const cookies = request.headers.get("Cookie") || ""; 279 | const authToken = cookies.match(/auth_token=([^;]+)/); // 获取cookie中的auth_token 280 | if (authToken) { 281 | try { 282 | // 解码token,验证是否过期 283 | const tokenData = JSON.parse(atob(authToken[1])); 284 | const now = Date.now(); 285 | // 检查token是否过期 286 | if (now > tokenData.expiration) { 287 | console.log("Token已过期"); 288 | return false; 289 | } 290 | // 如果token有效,返回用户名是否匹配 291 | return tokenData.username === config.username; 292 | } catch (error) { 293 | console.error("Token的用户名不匹配", error); 294 | return false; 295 | } 296 | } 297 | return false; 298 | } 299 | 300 | // 处理路由 301 | async function handleAuthRequest(request, config) { 302 | if (config.enableAuth) { 303 | // 使用 authenticate 函数检查用户是否已认证 304 | const isAuthenticated = authenticate(request, config); 305 | if (!isAuthenticated) { 306 | return handleLoginRequest(request, config); // 认证失败,跳转到登录页面 307 | } 308 | return handleUploadRequest(request, config); // 认证通过,跳转到上传页面 309 | } 310 | // 如果没有启用认证,直接跳转到上传页面 311 | return handleUploadRequest(request, config); 312 | } 313 | 314 | // 处理登录 315 | async function handleLoginRequest(request, config) { 316 | if (request.method === 'POST') { 317 | const { username, password } = await request.json(); 318 | 319 | if (username === config.username && password === config.password) { 320 | // 登录成功,设置一个有效期7天的cookie 321 | const expirationDate = new Date(); 322 | expirationDate.setDate(expirationDate.getDate() + config.cookie); 323 | const expirationTimestamp = expirationDate.getTime(); 324 | // 创建token数据,包含用户名和过期时间 325 | const tokenData = JSON.stringify({ 326 | username: config.username, 327 | expiration: expirationTimestamp 328 | }); 329 | 330 | const token = btoa(tokenData); // Base64编码 331 | const cookie = `auth_token=${token}; Path=/; HttpOnly; Secure; Expires=${expirationDate.toUTCString()}`; 332 | return new Response("登录成功", { 333 | status: 200, 334 | headers: { 335 | "Set-Cookie": cookie, 336 | "Content-Type": "text/plain" 337 | } 338 | }); 339 | } 340 | return new Response("认证失败", { status: 401 }); 341 | } 342 | const html = generateLoginPage(); // 如果是GET请求,返回登录页面 343 | return new Response(html, { 344 | headers: { 'Content-Type': 'text/html;charset=UTF-8' } 345 | }); 346 | } 347 | 348 | // 处理文件上传 349 | async function handleUploadRequest(request, config) { 350 | if (config.enableAuth && !authenticate(request, config)) { 351 | return Response.redirect(`${new URL(request.url).origin}/`, 302); 352 | } 353 | if (request.method === 'GET') { 354 | const html = generateUploadPage(); 355 | return new Response(html, { 356 | headers: { 'Content-Type': 'text/html;charset=UTF-8' } 357 | }); 358 | } 359 | 360 | try { 361 | const formData = await request.formData(); 362 | const file = formData.get('file'); 363 | if (!file) throw new Error('未找到文件'); 364 | if (file.size > config.maxSizeMB * 1024 * 1024) throw new Error(`文件超过${config.maxSizeMB}MB限制`); 365 | 366 | const ext = (file.name.split('.').pop() || '').toLowerCase(); //获取文件扩展名 367 | const mimeType = getContentType(ext); // 获取文件类型 368 | const [mainType] = mimeType.split('/'); // 获取主类型 369 | // 定义类型映射 370 | const typeMap = { 371 | image: { method: 'sendPhoto', field: 'photo' }, 372 | video: { method: 'sendVideo', field: 'video' }, 373 | audio: { method: 'sendAudio', field: 'audio' } 374 | }; 375 | let { method = 'sendDocument', field = 'document' } = typeMap[mainType] || {}; 376 | 377 | if (['application', 'text'].includes(mainType)) { 378 | method = 'sendDocument'; 379 | field = 'document'; 380 | } 381 | 382 | const tgFormData = new FormData(); 383 | tgFormData.append('chat_id', config.tgChatId); 384 | tgFormData.append(field, file, file.name); 385 | const tgResponse = await fetch( 386 | `https://api.telegram.org/bot${config.tgBotToken}/${method}`, 387 | { method: 'POST', body: tgFormData } 388 | ); 389 | if (!tgResponse.ok) throw new Error('Telegram参数配置错误'); 390 | 391 | const tgData = await tgResponse.json(); 392 | const result = tgData.result; 393 | const messageId = tgData.result?.message_id; 394 | const fileId = result?.document?.file_id || 395 | result?.video?.file_id || 396 | result?.audio?.file_id || 397 | (result?.photo && result.photo[result.photo.length-1]?.file_id); 398 | if (!fileId) throw new Error('未获取到文件ID'); 399 | if (!messageId) throw new Error('未获取到tg消息ID'); 400 | 401 | const time = Date.now(); 402 | const timestamp = new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString(); 403 | const url = `https://${config.domain}/${time}.${ext}`; 404 | // const datetime = timestamp.split('T')[0].replace(/-/g, ''); // 获取ISO时间戳的纯数字日期 405 | // const url = `https://${config.domain}/${datetime}-${time}.${ext}`;  406 | 407 | await config.database.prepare(` 408 | INSERT INTO files (url, fileId, message_id, created_at, file_name, file_size, mime_type) 409 | VALUES (?, ?, ?, ?, ?, ?, ?) 410 | `).bind( 411 | url, 412 | fileId, 413 | messageId, 414 | timestamp, 415 | file.name, 416 | file.size, 417 | file.type || getContentType(ext) 418 | ).run(); 419 | 420 | return new Response( 421 | JSON.stringify({ status: 1, msg: "✔ 上传成功", url }), 422 | { headers: { 'Content-Type': 'application/json' }} 423 | ); 424 | 425 | } catch (error) { 426 | console.error(`[Upload Error] ${error.message}`); 427 | // 根据错误信息设定不同的状态码 428 | let statusCode = 500; // 默认500 429 | if (error.message.includes(`文件超过${config.maxSizeMB}MB限制`)) { 430 | statusCode = 400; // 客户端错误:文件大小超限 431 | } else if (error.message.includes('Telegram参数配置错误')) { 432 | statusCode = 502; // 网关错误:与Telegram通信失败 433 | } else if (error.message.includes('未获取到文件ID') || error.message.includes('未获取到tg消息ID')) { 434 | statusCode = 500; // 服务器内部错误:Telegram返回数据异常 435 | } else if (error instanceof TypeError && error.message.includes('Failed to fetch')) { 436 | statusCode = 504; // 网络超时或断网 437 | } 438 | return new Response( 439 | JSON.stringify({ status: 0, msg: "✘ 上传失败", error: error.message }), 440 | { status: statusCode, headers: { 'Content-Type': 'application/json' }} 441 | ); 442 | } 443 | } 444 | 445 | // 处理文件管理和预览 446 | async function handleAdminRequest(request, config) { 447 | if (config.enableAuth && !authenticate(request, config)) { 448 | return Response.redirect(`${new URL(request.url).origin}/`, 302); 449 | } 450 | 451 | const files = await config.database.prepare( 452 | `SELECT url, fileId, message_id, created_at, file_name, file_size, mime_type 453 | FROM files 454 | ORDER BY created_at DESC` 455 | ).all(); 456 | 457 | const fileList = files.results || []; 458 | const fileCards = fileList.map(file => { 459 | const fileName = file.file_name; 460 | const fileSize = formatSize(file.file_size || 0); 461 | const createdAt = new Date(file.created_at).toISOString().replace('T', ' ').split('.')[0]; 462 | // 文件预览信息和操作元素 463 | return ` 464 |
465 | 466 | 467 |
468 | ${getPreviewHtml(file.url)} 469 |
470 |
471 |
${fileName}
472 |
${fileSize}
473 |
${createdAt}
474 |
475 |
476 | 477 | 下载 478 | 479 |
480 |
481 | `; 482 | }).join(''); 483 | 484 | // 二维码分享元素 485 | const qrModal = ` 486 |
487 |
488 |
489 |
490 | 491 | 492 |
493 |
494 |
495 | `; 496 | 497 | const html = generateAdminPage(fileCards, qrModal); 498 | return new Response(html, { 499 | headers: { 'Content-Type': 'text/html;charset=UTF-8' } 500 | }); 501 | } 502 | 503 | // 处理文件搜索 504 | async function handleSearchRequest(request, config) { 505 | if (config.enableAuth && !authenticate(request, config)) { 506 | return Response.redirect(`${new URL(request.url).origin}/`, 302); 507 | } 508 | 509 | try { 510 | const { query } = await request.json(); 511 | const searchPattern = `%${query}%`; 512 | const files = await config.database.prepare( 513 | `SELECT url, fileId, message_id, created_at, file_name, file_size, mime_type 514 | FROM files 515 | WHERE file_name LIKE ? ESCAPE '!' 516 | COLLATE NOCASE 517 | ORDER BY created_at DESC` 518 | ).bind(searchPattern).all(); 519 | 520 | return new Response( 521 | JSON.stringify({ files: files.results || [] }), 522 | { headers: { 'Content-Type': 'application/json' }} 523 | ); 524 | 525 | } catch (error) { 526 | console.error(`[Search Error] ${error.message}`); 527 | return new Response( 528 | JSON.stringify({ error: error.message }), 529 | { status: 500, headers: { 'Content-Type': 'application/json' }} 530 | ); 531 | } 532 | } 533 | 534 | // 支持预览的文件类型 535 | function getPreviewHtml(url) { 536 | const ext = (url.split('.').pop() || '').toLowerCase(); 537 | const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'icon'].includes(ext); 538 | const isVideo = ['mp4', 'webm'].includes(ext); 539 | const isAudio = ['mp3', 'wav', 'ogg'].includes(ext); 540 | 541 | if (isImage) { 542 | return `预览`; 543 | } else if (isVideo) { 544 | return ``; 545 | } else if (isAudio) { 546 | return ``; 547 | } else { 548 | return `
📄
`; 549 | } 550 | } 551 | 552 | // 获取文件并缓存 553 | async function handleFileRequest(request, config) { 554 | const url = request.url; 555 | const cache = caches.default; 556 | const cacheKey = new Request(url); 557 | 558 | try { 559 | // 尝试从缓存获取 560 | const cachedResponse = await cache.match(cacheKey); 561 | if (cachedResponse) { 562 | console.log(`[Cache Hit] ${url}`); 563 | return cachedResponse; 564 | } 565 | 566 | // 从数据库查询文件 567 | const file = await config.database.prepare( 568 | `SELECT fileId, message_id, file_name, mime_type 569 | FROM files WHERE url = ?` 570 | ).bind(url).first(); 571 | 572 | if (!file) { 573 | console.log(`[404] File not found: ${url}`); 574 | return new Response('文件不存在', { 575 | status: 404, 576 | headers: { 'Content-Type': 'text/plain;charset=UTF-8' } 577 | }); 578 | } 579 | 580 | // 获取 Telegram 文件路径 581 | const tgResponse = await fetch( 582 | `https://api.telegram.org/bot${config.tgBotToken}/getFile?file_id=${file.fileId}` 583 | ); 584 | 585 | if (!tgResponse.ok) { 586 | console.error(`[Telegram API Error] ${await tgResponse.text()} for file ${file.fileId}`); 587 | return new Response('获取文件失败', { 588 | status: 500, 589 | headers: { 'Content-Type': 'text/plain;charset=UTF-8' } 590 | }); 591 | } 592 | 593 | const tgData = await tgResponse.json(); 594 | const filePath = tgData.result?.file_path; 595 | 596 | if (!filePath) { 597 | console.error(`[Invalid Path] No file_path in response for ${file.fileId}`); 598 | return new Response('文件路径无效', { 599 | status: 404, 600 | headers: { 'Content-Type': 'text/plain;charset=UTF-8' } 601 | }); 602 | } 603 | 604 | // 下载文件 605 | const fileUrl = `https://api.telegram.org/file/bot${config.tgBotToken}/${filePath}`; 606 | const fileResponse = await fetch(fileUrl); 607 | 608 | if (!fileResponse.ok) { 609 | console.error(`[Download Error] Failed to download from ${fileUrl}`); 610 | return new Response('下载文件失败', { 611 | status: 500, 612 | headers: { 'Content-Type': 'text/plain;charset=UTF-8' } 613 | }); 614 | } 615 | 616 | // 使用存储的 MIME 类型或根据扩展名判断 617 | const contentType = file.mime_type || getContentType(url.split('.').pop().toLowerCase()); 618 | 619 | // 创建响应并缓存 620 | const response = new Response(fileResponse.body, { 621 | headers: { 622 | 'Content-Type': contentType, 623 | 'Cache-Control': 'public, max-age=31536000', 624 | 'X-Content-Type-Options': 'nosniff', 625 | 'Access-Control-Allow-Origin': '*', 626 | 'Content-Disposition': `inline; filename*=UTF-8''${encodeURIComponent(file.file_name || '')}` 627 | } 628 | }); 629 | 630 | await cache.put(cacheKey, response.clone()); 631 | console.log(`[Cache Set] ${url}`); 632 | return response; 633 | 634 | } catch (error) { 635 | console.error(`[Error] ${error.message} for ${url}`); 636 | return new Response('服务器内部错误', { 637 | status: 500, 638 | headers: { 'Content-Type': 'text/plain;charset=UTF-8' } 639 | }); 640 | } 641 | } 642 | 643 | // 处理文件删除 644 | async function handleDeleteRequest(request, config) { 645 | if (config.enableAuth && !authenticate(request, config)) { 646 | return Response.redirect(`${new URL(request.url).origin}/`, 302); 647 | } 648 | 649 | try { 650 | const { url } = await request.json(); 651 | if (!url || typeof url !== 'string') { 652 | return new Response(JSON.stringify({ error: '无效的URL' }), { 653 | status: 400, 654 | headers: { 'Content-Type': 'application/json' } 655 | }); 656 | } 657 | 658 | const file = await config.database.prepare( 659 | 'SELECT fileId, message_id FROM files WHERE url = ?' 660 | ).bind(url).first(); 661 | if (!file) { 662 | return new Response(JSON.stringify({ error: '文件不存在' }), { 663 | status: 404, 664 | headers: { 'Content-Type': 'application/json' } 665 | }); 666 | } 667 | 668 | let deleteError = null; 669 | 670 | try { 671 | const deleteResponse = await fetch( 672 | `https://api.telegram.org/bot${config.tgBotToken}/deleteMessage?chat_id=${config.tgChatId}&message_id=${file.message_id}` 673 | ); 674 | if (!deleteResponse.ok) { 675 | const errorData = await deleteResponse.json(); 676 | console.error(`[Telegram API Error] ${JSON.stringify(errorData)}`); 677 | throw new Error(`Telegram 消息删除失败: ${errorData.description}`); 678 | } 679 | } catch (error) { deleteError = error.message; } 680 | 681 | // 删除数据库表数据,即使Telegram删除失败也会删除数据库记录 682 | await config.database.prepare('DELETE FROM files WHERE url = ?').bind(url).run(); 683 | 684 | return new Response( 685 | JSON.stringify({ 686 | success: true, 687 | message: deleteError ? `文件已从数据库删除,但Telegram消息删除失败: ${deleteError}` : '文件删除成功' 688 | }), 689 | { headers: { 'Content-Type': 'application/json' }} 690 | ); 691 | 692 | } catch (error) { 693 | console.error(`[Delete Error] ${error.message}`); 694 | return new Response( 695 | JSON.stringify({ 696 | error: error.message.includes('message to delete not found') ? 697 | '文件已从频道移除' : error.message 698 | }), 699 | { status: 500, headers: { 'Content-Type': 'application/json' }} 700 | ); 701 | } 702 | } 703 | 704 | // 支持上传的文件类型 705 | function getContentType(ext) { 706 | const types = { 707 | jpg: 'image/jpeg', 708 | jpeg: 'image/jpeg', 709 | png: 'image/png', 710 | gif: 'image/gif', 711 | webp: 'image/webp', 712 | svg: 'image/svg+xml', 713 | icon: 'image/x-icon', 714 | mp4: 'video/mp4', 715 | webm: 'video/webm', 716 | mp3: 'audio/mpeg', 717 | wav: 'audio/wav', 718 | ogg: 'audio/ogg', 719 | pdf: 'application/pdf', 720 | txt: 'text/plain', 721 | md: 'text/markdown', 722 | zip: 'application/zip', 723 | rar: 'application/x-rar-compressed', 724 | json: 'application/json', 725 | xml: 'application/xml', 726 | ini: 'text/plain', 727 | js: 'application/javascript', 728 | yml: 'application/yaml', 729 | yaml: 'application/yaml', 730 | py: 'text/x-python', 731 | sh: 'application/x-sh' 732 | }; 733 | return types[ext] || 'application/octet-stream'; 734 | } 735 | 736 | async function handleBingImagesRequest() { 737 | const cache = caches.default; 738 | const cacheKey = new Request('https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=5'); 739 | 740 | const cachedResponse = await cache.match(cacheKey); 741 | if (cachedResponse) { 742 | console.log('Returning cached response'); 743 | return cachedResponse; 744 | } 745 | 746 | try { 747 | const res = await fetch(cacheKey); 748 | if (!res.ok) { 749 | console.error(`Bing API 请求失败,状态码:${res.status}`); 750 | return new Response('请求 Bing API 失败', { status: res.status }); 751 | } 752 | 753 | const bingData = await res.json(); 754 | const images = bingData.images.map(image => ({ url: `https://cn.bing.com${image.url}` })); 755 | const returnData = { status: true, message: "操作成功", data: images }; 756 | 757 | const response = new Response(JSON.stringify(returnData), { 758 | status: 200, 759 | headers: { 760 | 'Content-Type': 'application/json', 761 | 'Cache-Control': 'public, max-age=21600', 762 | 'Access-Control-Allow-Origin': '*' 763 | } 764 | }); 765 | 766 | await cache.put(cacheKey, response.clone()); 767 | console.log('响应数据已缓存'); 768 | return response; 769 | } catch (error) { 770 | console.error('请求 Bing API 过程中发生错误:', error); 771 | return new Response('请求 Bing API 失败', { status: 500 }); 772 | } 773 | } 774 | 775 | // 文件大小计算函数 776 | function formatSize(bytes) { 777 | const units = ['B', 'KB', 'MB', 'GB']; 778 | let size = bytes; 779 | let unitIndex = 0; 780 | while (size >= 1024 && unitIndex < units.length - 1) { 781 | size /= 1024; 782 | unitIndex++; 783 | } 784 | return `${size.toFixed(2)} ${units[unitIndex]}`; 785 | } 786 | 787 | // 登录页面生成函数 /login 788 | async function generateLoginPage() { 789 | const baseHtml = await loadTemplate('login.html'); 790 | return render(baseHtml, { pageTitle: '用户登录', }); 791 | } 792 | 793 | // 生成文件上传页面 /upload 794 | async function generateUploadPage() { 795 | const baseHtml = await loadTemplate('upload.html'); 796 | return render(baseHtml, { 797 | pageTitle: '文件上传', 798 | githubUrl:'https://github.com/yutian81/CF-tgfile', 799 | githubName:'Yutian81 GitHub', 800 | blogUrl:'https://blog.811520.xyz/', 801 | blogName:'青云志Blog' 802 | }); 803 | } 804 | 805 | // 生成文件管理页面 /admin 806 | async function generateAdminPage(fileCards, qrModal) { 807 | const baseHtml = await loadTemplate('admin.html'); 808 | return render(baseHtml, { 809 | pageTitle: '文件管理', 810 | FILE_CARDS: fileCards.join(''), 811 | QR_MODAL: qrModal 812 | }); 813 | } 814 | -------------------------------------------------------------------------------- /_worker.js: -------------------------------------------------------------------------------- 1 | // 由于tg的限制,虽然可以上传超过20M的文件,但无法返回直链地址 2 | // 因此修改代码,当文件大于20MB时,直接阻止上传 3 | 4 | // 数据库初始化函数 5 | async function initDatabase(config) { 6 | await config.database.prepare(` 7 | CREATE TABLE IF NOT EXISTS files ( 8 | url TEXT PRIMARY KEY, 9 | fileId TEXT NOT NULL, 10 | message_id INTEGER NOT NULL, 11 | created_at INTEGER NOT NULL, 12 | file_name TEXT, 13 | file_size INTEGER, 14 | mime_type TEXT 15 | ) 16 | `).run(); 17 | } 18 | 19 | // 导出函数 20 | export default { 21 | async fetch(request, env) { 22 | // 环境变量配置 23 | const config = { 24 | domain: env.DOMAIN, 25 | database: env.DATABASE, 26 | username: env.USERNAME, 27 | password: env.PASSWORD, 28 | enableAuth: env.ENABLE_AUTH === 'true', 29 | tgBotToken: env.TG_BOT_TOKEN, 30 | tgChatId: env.TG_CHAT_ID, 31 | cookie: Number(env.COOKIE) || 7, // cookie有效期默认为 7 32 | maxSizeMB: Number(env.MAX_SIZE_MB) || 20 // 上传单文件大小默认为20M 33 | }; 34 | 35 | // 初始化数据库 36 | await initDatabase(config); 37 | // 路由处理 38 | const { pathname } = new URL(request.url); 39 | 40 | if (pathname === '/config') { 41 | const safeConfig = { maxSizeMB: config.maxSizeMB }; 42 | return new Response(JSON.stringify(safeConfig), { 43 | headers: { 'Content-Type': 'application/json' } 44 | }); 45 | } 46 | 47 | const routes = { 48 | '/': () => handleAuthRequest(request, config), 49 | '/login': () => handleLoginRequest(request, config), 50 | '/upload': () => handleUploadRequest(request, config), 51 | '/admin': () => handleAdminRequest(request, config), 52 | '/delete': () => handleDeleteRequest(request, config), 53 | '/search': () => handleSearchRequest(request, config), 54 | '/bing': handleBingImagesRequest 55 | }; 56 | const handler = routes[pathname]; 57 | if (handler) { 58 | return await handler(); 59 | } 60 | // 处理文件访问请求 61 | return await handleFileRequest(request, config); 62 | } 63 | }; 64 | 65 | // 处理身份认证 66 | function authenticate(request, config) { 67 | const cookies = request.headers.get("Cookie") || ""; 68 | const authToken = cookies.match(/auth_token=([^;]+)/); // 获取cookie中的auth_token 69 | if (authToken) { 70 | try { 71 | // 解码token,验证是否过期 72 | const tokenData = JSON.parse(atob(authToken[1])); 73 | const now = Date.now(); 74 | // 检查token是否过期 75 | if (now > tokenData.expiration) { 76 | console.log("Token已过期"); 77 | return false; 78 | } 79 | // 如果token有效,返回用户名是否匹配 80 | return tokenData.username === config.username; 81 | } catch (error) { 82 | console.error("Token的用户名不匹配", error); 83 | return false; 84 | } 85 | } 86 | return false; 87 | } 88 | 89 | // 处理路由 90 | async function handleAuthRequest(request, config) { 91 | if (config.enableAuth) { 92 | // 使用 authenticate 函数检查用户是否已认证 93 | const isAuthenticated = authenticate(request, config); 94 | if (!isAuthenticated) { 95 | return handleLoginRequest(request, config); // 认证失败,跳转到登录页面 96 | } 97 | return handleUploadRequest(request, config); // 认证通过,跳转到上传页面 98 | } 99 | // 如果没有启用认证,直接跳转到上传页面 100 | return handleUploadRequest(request, config); 101 | } 102 | 103 | // 处理登录 104 | async function handleLoginRequest(request, config) { 105 | if (request.method === 'POST') { 106 | const { username, password } = await request.json(); 107 | 108 | if (username === config.username && password === config.password) { 109 | // 登录成功,设置一个有效期7天的cookie 110 | const expirationDate = new Date(); 111 | expirationDate.setDate(expirationDate.getDate() + config.cookie); 112 | const expirationTimestamp = expirationDate.getTime(); 113 | // 创建token数据,包含用户名和过期时间 114 | const tokenData = JSON.stringify({ 115 | username: config.username, 116 | expiration: expirationTimestamp 117 | }); 118 | 119 | const token = btoa(tokenData); // Base64编码 120 | const cookie = `auth_token=${token}; Path=/; HttpOnly; Secure; Expires=${expirationDate.toUTCString()}`; 121 | return new Response("登录成功", { 122 | status: 200, 123 | headers: { 124 | "Set-Cookie": cookie, 125 | "Content-Type": "text/plain" 126 | } 127 | }); 128 | } 129 | return new Response("认证失败", { status: 401 }); 130 | } 131 | const html = generateLoginPage(); // 如果是GET请求,返回登录页面 132 | return new Response(html, { 133 | headers: { 'Content-Type': 'text/html;charset=UTF-8' } 134 | }); 135 | } 136 | 137 | // 处理文件上传 138 | async function handleUploadRequest(request, config) { 139 | if (config.enableAuth && !authenticate(request, config)) { 140 | return Response.redirect(`${new URL(request.url).origin}/`, 302); 141 | } 142 | if (request.method === 'GET') { 143 | const html = generateUploadPage(); 144 | return new Response(html, { 145 | headers: { 'Content-Type': 'text/html;charset=UTF-8' } 146 | }); 147 | } 148 | 149 | try { 150 | const formData = await request.formData(); 151 | const file = formData.get('file'); 152 | if (!file) throw new Error('未找到文件'); 153 | if (file.size > config.maxSizeMB * 1024 * 1024) throw new Error(`文件超过${config.maxSizeMB}MB限制`); 154 | 155 | const ext = (file.name.split('.').pop() || '').toLowerCase(); //获取文件扩展名 156 | const mimeType = getContentType(ext); // 获取文件类型 157 | const [mainType] = mimeType.split('/'); // 获取主类型 158 | // 定义类型映射 159 | const typeMap = { 160 | image: { method: 'sendPhoto', field: 'photo' }, 161 | video: { method: 'sendVideo', field: 'video' }, 162 | audio: { method: 'sendAudio', field: 'audio' } 163 | }; 164 | let { method = 'sendDocument', field = 'document' } = typeMap[mainType] || {}; 165 | 166 | if (['application', 'text'].includes(mainType)) { 167 | method = 'sendDocument'; 168 | field = 'document'; 169 | } 170 | 171 | const tgFormData = new FormData(); 172 | tgFormData.append('chat_id', config.tgChatId); 173 | tgFormData.append(field, file, file.name); 174 | const tgResponse = await fetch( 175 | `https://api.telegram.org/bot${config.tgBotToken}/${method}`, 176 | { method: 'POST', body: tgFormData } 177 | ); 178 | if (!tgResponse.ok) throw new Error('Telegram参数配置错误'); 179 | 180 | const tgData = await tgResponse.json(); 181 | const result = tgData.result; 182 | const messageId = tgData.result?.message_id; 183 | const fileId = result?.document?.file_id || 184 | result?.video?.file_id || 185 | result?.audio?.file_id || 186 | (result?.photo && result.photo[result.photo.length-1]?.file_id); 187 | if (!fileId) throw new Error('未获取到文件ID'); 188 | if (!messageId) throw new Error('未获取到tg消息ID'); 189 | 190 | const time = Date.now(); 191 | const timestamp = new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString(); 192 | const url = `https://${config.domain}/${time}.${ext}`; 193 | // const datetime = timestamp.split('T')[0].replace(/-/g, ''); // 获取ISO时间戳的纯数字日期 194 | // const url = `https://${config.domain}/${datetime}-${time}.${ext}`;  195 | 196 | await config.database.prepare(` 197 | INSERT INTO files (url, fileId, message_id, created_at, file_name, file_size, mime_type) 198 | VALUES (?, ?, ?, ?, ?, ?, ?) 199 | `).bind( 200 | url, 201 | fileId, 202 | messageId, 203 | timestamp, 204 | file.name, 205 | file.size, 206 | file.type || getContentType(ext) 207 | ).run(); 208 | 209 | return new Response( 210 | JSON.stringify({ status: 1, msg: "✔ 上传成功", url }), 211 | { headers: { 'Content-Type': 'application/json' }} 212 | ); 213 | 214 | } catch (error) { 215 | console.error(`[Upload Error] ${error.message}`); 216 | // 根据错误信息设定不同的状态码 217 | let statusCode = 500; // 默认500 218 | if (error.message.includes(`文件超过${config.maxSizeMB}MB限制`)) { 219 | statusCode = 400; // 客户端错误:文件大小超限 220 | } else if (error.message.includes('Telegram参数配置错误')) { 221 | statusCode = 502; // 网关错误:与Telegram通信失败 222 | } else if (error.message.includes('未获取到文件ID') || error.message.includes('未获取到tg消息ID')) { 223 | statusCode = 500; // 服务器内部错误:Telegram返回数据异常 224 | } else if (error instanceof TypeError && error.message.includes('Failed to fetch')) { 225 | statusCode = 504; // 网络超时或断网 226 | } 227 | return new Response( 228 | JSON.stringify({ status: 0, msg: "✘ 上传失败", error: error.message }), 229 | { status: statusCode, headers: { 'Content-Type': 'application/json' }} 230 | ); 231 | } 232 | } 233 | 234 | // 处理文件管理和预览 235 | async function handleAdminRequest(request, config) { 236 | if (config.enableAuth && !authenticate(request, config)) { 237 | return Response.redirect(`${new URL(request.url).origin}/`, 302); 238 | } 239 | 240 | const files = await config.database.prepare( 241 | `SELECT url, fileId, message_id, created_at, file_name, file_size, mime_type 242 | FROM files 243 | ORDER BY created_at DESC` 244 | ).all(); 245 | 246 | const fileList = files.results || []; 247 | const fileCards = fileList.map(file => { 248 | const fileName = file.file_name; 249 | const fileSize = formatSize(file.file_size || 0); 250 | const createdAt = new Date(file.created_at).toISOString().replace('T', ' ').split('.')[0]; 251 | // 文件预览信息和操作元素 252 | return ` 253 |
254 | 255 | 256 |
257 | ${getPreviewHtml(file.url)} 258 |
259 |
260 |
${fileName}
261 |
${fileSize}
262 |
${createdAt}
263 |
264 |
265 | 266 | 下载 267 | 268 |
269 |
270 | `; 271 | }).join(''); 272 | 273 | // 二维码分享元素 274 | const qrModal = ` 275 |
276 |
277 |
278 |
279 | 280 | 281 |
282 |
283 |
284 | `; 285 | 286 | const html = generateAdminPage(fileCards, qrModal); 287 | return new Response(html, { 288 | headers: { 'Content-Type': 'text/html;charset=UTF-8' } 289 | }); 290 | } 291 | 292 | // 处理文件搜索 293 | async function handleSearchRequest(request, config) { 294 | if (config.enableAuth && !authenticate(request, config)) { 295 | return Response.redirect(`${new URL(request.url).origin}/`, 302); 296 | } 297 | 298 | try { 299 | const { query } = await request.json(); 300 | const searchPattern = `%${query}%`; 301 | const files = await config.database.prepare( 302 | `SELECT url, fileId, message_id, created_at, file_name, file_size, mime_type 303 | FROM files 304 | WHERE file_name LIKE ? ESCAPE '!' 305 | COLLATE NOCASE 306 | ORDER BY created_at DESC` 307 | ).bind(searchPattern).all(); 308 | 309 | return new Response( 310 | JSON.stringify({ files: files.results || [] }), 311 | { headers: { 'Content-Type': 'application/json' }} 312 | ); 313 | 314 | } catch (error) { 315 | console.error(`[Search Error] ${error.message}`); 316 | return new Response( 317 | JSON.stringify({ error: error.message }), 318 | { status: 500, headers: { 'Content-Type': 'application/json' }} 319 | ); 320 | } 321 | } 322 | 323 | // 支持预览的文件类型 324 | function getPreviewHtml(url) { 325 | const ext = (url.split('.').pop() || '').toLowerCase(); 326 | const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'icon'].includes(ext); 327 | const isVideo = ['mp4', 'webm'].includes(ext); 328 | const isAudio = ['mp3', 'wav', 'ogg'].includes(ext); 329 | 330 | if (isImage) { 331 | return `预览`; 332 | } else if (isVideo) { 333 | return ``; 334 | } else if (isAudio) { 335 | return ``; 336 | } else { 337 | return `
📄
`; 338 | } 339 | } 340 | 341 | // 获取文件并缓存 342 | async function handleFileRequest(request, config) { 343 | const url = request.url; 344 | const cache = caches.default; 345 | const cacheKey = new Request(url); 346 | 347 | try { 348 | // 尝试从缓存获取 349 | const cachedResponse = await cache.match(cacheKey); 350 | if (cachedResponse) { 351 | console.log(`[Cache Hit] ${url}`); 352 | return cachedResponse; 353 | } 354 | 355 | // 从数据库查询文件 356 | const file = await config.database.prepare( 357 | `SELECT fileId, message_id, file_name, mime_type 358 | FROM files WHERE url = ?` 359 | ).bind(url).first(); 360 | 361 | if (!file) { 362 | console.log(`[404] File not found: ${url}`); 363 | return new Response('文件不存在', { 364 | status: 404, 365 | headers: { 'Content-Type': 'text/plain;charset=UTF-8' } 366 | }); 367 | } 368 | 369 | // 获取 Telegram 文件路径 370 | const tgResponse = await fetch( 371 | `https://api.telegram.org/bot${config.tgBotToken}/getFile?file_id=${file.fileId}` 372 | ); 373 | 374 | if (!tgResponse.ok) { 375 | console.error(`[Telegram API Error] ${await tgResponse.text()} for file ${file.fileId}`); 376 | return new Response('获取文件失败', { 377 | status: 500, 378 | headers: { 'Content-Type': 'text/plain;charset=UTF-8' } 379 | }); 380 | } 381 | 382 | const tgData = await tgResponse.json(); 383 | const filePath = tgData.result?.file_path; 384 | 385 | if (!filePath) { 386 | console.error(`[Invalid Path] No file_path in response for ${file.fileId}`); 387 | return new Response('文件路径无效', { 388 | status: 404, 389 | headers: { 'Content-Type': 'text/plain;charset=UTF-8' } 390 | }); 391 | } 392 | 393 | // 下载文件 394 | const fileUrl = `https://api.telegram.org/file/bot${config.tgBotToken}/${filePath}`; 395 | const fileResponse = await fetch(fileUrl); 396 | 397 | if (!fileResponse.ok) { 398 | console.error(`[Download Error] Failed to download from ${fileUrl}`); 399 | return new Response('下载文件失败', { 400 | status: 500, 401 | headers: { 'Content-Type': 'text/plain;charset=UTF-8' } 402 | }); 403 | } 404 | 405 | // 使用存储的 MIME 类型或根据扩展名判断 406 | const contentType = file.mime_type || getContentType(url.split('.').pop().toLowerCase()); 407 | 408 | // 创建响应并缓存 409 | const response = new Response(fileResponse.body, { 410 | headers: { 411 | 'Content-Type': contentType, 412 | 'Cache-Control': 'public, max-age=31536000', 413 | 'X-Content-Type-Options': 'nosniff', 414 | 'Access-Control-Allow-Origin': '*', 415 | 'Content-Disposition': `inline; filename*=UTF-8''${encodeURIComponent(file.file_name || '')}` 416 | } 417 | }); 418 | 419 | await cache.put(cacheKey, response.clone()); 420 | console.log(`[Cache Set] ${url}`); 421 | return response; 422 | 423 | } catch (error) { 424 | console.error(`[Error] ${error.message} for ${url}`); 425 | return new Response('服务器内部错误', { 426 | status: 500, 427 | headers: { 'Content-Type': 'text/plain;charset=UTF-8' } 428 | }); 429 | } 430 | } 431 | 432 | // 处理文件删除 433 | async function handleDeleteRequest(request, config) { 434 | if (config.enableAuth && !authenticate(request, config)) { 435 | return Response.redirect(`${new URL(request.url).origin}/`, 302); 436 | } 437 | 438 | try { 439 | const { url } = await request.json(); 440 | if (!url || typeof url !== 'string') { 441 | return new Response(JSON.stringify({ error: '无效的URL' }), { 442 | status: 400, 443 | headers: { 'Content-Type': 'application/json' } 444 | }); 445 | } 446 | 447 | const file = await config.database.prepare( 448 | 'SELECT fileId, message_id FROM files WHERE url = ?' 449 | ).bind(url).first(); 450 | if (!file) { 451 | return new Response(JSON.stringify({ error: '文件不存在' }), { 452 | status: 404, 453 | headers: { 'Content-Type': 'application/json' } 454 | }); 455 | } 456 | 457 | let deleteError = null; 458 | 459 | try { 460 | const deleteResponse = await fetch( 461 | `https://api.telegram.org/bot${config.tgBotToken}/deleteMessage?chat_id=${config.tgChatId}&message_id=${file.message_id}` 462 | ); 463 | if (!deleteResponse.ok) { 464 | const errorData = await deleteResponse.json(); 465 | console.error(`[Telegram API Error] ${JSON.stringify(errorData)}`); 466 | throw new Error(`Telegram 消息删除失败: ${errorData.description}`); 467 | } 468 | } catch (error) { deleteError = error.message; } 469 | 470 | // 删除数据库表数据,即使Telegram删除失败也会删除数据库记录 471 | await config.database.prepare('DELETE FROM files WHERE url = ?').bind(url).run(); 472 | 473 | return new Response( 474 | JSON.stringify({ 475 | success: true, 476 | message: deleteError ? `文件已从数据库删除,但Telegram消息删除失败: ${deleteError}` : '文件删除成功' 477 | }), 478 | { headers: { 'Content-Type': 'application/json' }} 479 | ); 480 | 481 | } catch (error) { 482 | console.error(`[Delete Error] ${error.message}`); 483 | return new Response( 484 | JSON.stringify({ 485 | error: error.message.includes('message to delete not found') ? 486 | '文件已从频道移除' : error.message 487 | }), 488 | { status: 500, headers: { 'Content-Type': 'application/json' }} 489 | ); 490 | } 491 | } 492 | 493 | // 支持上传的文件类型 494 | function getContentType(ext) { 495 | const types = { 496 | jpg: 'image/jpeg', 497 | jpeg: 'image/jpeg', 498 | png: 'image/png', 499 | gif: 'image/gif', 500 | webp: 'image/webp', 501 | svg: 'image/svg+xml', 502 | icon: 'image/x-icon', 503 | mp4: 'video/mp4', 504 | webm: 'video/webm', 505 | mp3: 'audio/mpeg', 506 | wav: 'audio/wav', 507 | ogg: 'audio/ogg', 508 | pdf: 'application/pdf', 509 | txt: 'text/plain', 510 | md: 'text/markdown', 511 | zip: 'application/zip', 512 | rar: 'application/x-rar-compressed', 513 | json: 'application/json', 514 | xml: 'application/xml', 515 | ini: 'text/plain', 516 | js: 'application/javascript', 517 | yml: 'application/yaml', 518 | yaml: 'application/yaml', 519 | py: 'text/x-python', 520 | sh: 'application/x-sh' 521 | }; 522 | return types[ext] || 'application/octet-stream'; 523 | } 524 | 525 | async function handleBingImagesRequest() { 526 | const cache = caches.default; 527 | const cacheKey = new Request('https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=5'); 528 | 529 | const cachedResponse = await cache.match(cacheKey); 530 | if (cachedResponse) { 531 | console.log('Returning cached response'); 532 | return cachedResponse; 533 | } 534 | 535 | try { 536 | const res = await fetch(cacheKey); 537 | if (!res.ok) { 538 | console.error(`Bing API 请求失败,状态码:${res.status}`); 539 | return new Response('请求 Bing API 失败', { status: res.status }); 540 | } 541 | 542 | const bingData = await res.json(); 543 | const images = bingData.images.map(image => ({ url: `https://cn.bing.com${image.url}` })); 544 | const returnData = { status: true, message: "操作成功", data: images }; 545 | 546 | const response = new Response(JSON.stringify(returnData), { 547 | status: 200, 548 | headers: { 549 | 'Content-Type': 'application/json', 550 | 'Cache-Control': 'public, max-age=21600', 551 | 'Access-Control-Allow-Origin': '*' 552 | } 553 | }); 554 | 555 | await cache.put(cacheKey, response.clone()); 556 | console.log('响应数据已缓存'); 557 | return response; 558 | } catch (error) { 559 | console.error('请求 Bing API 过程中发生错误:', error); 560 | return new Response('请求 Bing API 失败', { status: 500 }); 561 | } 562 | } 563 | 564 | // 文件大小计算函数 565 | function formatSize(bytes) { 566 | const units = ['B', 'KB', 'MB', 'GB']; 567 | let size = bytes; 568 | let unitIndex = 0; 569 | while (size >= 1024 && unitIndex < units.length - 1) { 570 | size /= 1024; 571 | unitIndex++; 572 | } 573 | return `${size.toFixed(2)} ${units[unitIndex]}`; 574 | } 575 | 576 | // 登录页面生成函数 /login 577 | function generateLoginPage() { 578 | return ` 579 | 580 | 581 | 582 | 583 | 584 | 585 | 登录 586 | 636 | 637 | 638 |
639 |

登录

640 |
641 |
642 | 643 |
644 |
645 | 646 |
647 | 648 |
用户名或密码错误
649 |
650 |
651 | 693 | 694 | `; 695 | } 696 | 697 | // 生成文件上传页面 /upload 698 | function generateUploadPage() { 699 | return ` 700 | 701 | 702 | 703 | 704 | 705 | 706 | 文件上传 707 | 843 | 844 | 845 |
846 |
847 |

文件上传

848 | 进入管理页面 849 |
850 |
851 |

点击选择 或 拖拽文件到此处

852 | 853 |
854 |
855 |
856 | 857 |
858 |
859 | 860 | 861 | 862 |
863 | 869 |
870 |
871 |
872 | 873 | 1060 | 1061 | `; 1062 | } 1063 | 1064 | // 生成文件管理页面 /admin 1065 | function generateAdminPage(fileCards, qrModal) { 1066 | return ` 1067 | 1068 | 1069 | 1070 | 1071 | 1072 | 1073 | 文件管理 1074 | 1223 | 1224 | 1225 |
1226 |
1227 |

文件管理

1228 |
1229 | 返回 1230 | 1231 |
1232 |
1233 |
1234 | ${fileCards} 1235 |
1236 | ${qrModal} 1237 |
1238 | 1239 | 1240 | 1241 | 1242 | 1344 | 1345 | `; 1346 | } 1347 | -------------------------------------------------------------------------------- /html/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{pageTitle}} 9 | 158 | 159 | 160 |
161 |
162 |

{{pageTitle}}

163 |
164 | 返回 165 | 166 |
167 |
168 |
169 | {{FILE_CARDS}} 170 |
171 | {{QR_MODAL}} 172 |
173 | 174 | 175 | 176 | 177 | 279 | 280 | 281 | -------------------------------------------------------------------------------- /html/loader.js: -------------------------------------------------------------------------------- 1 | const templateCache = new Map(); 2 | 3 | export async function loadTemplate(name) { 4 | if (templateCache.has(name)) return templateCache.get(name); 5 | 6 | const response = await fetch(`/html/${name}`); 7 | const html = await response.text(); 8 | templateCache.set(name, html); 9 | return html; 10 | } 11 | 12 | export function render(template, data) { 13 | return Object.entries(data).reduce((result, [key, val]) => 14 | result.replace(new RegExp(`{{${key}}}`, 'g'), val), 15 | template); 16 | } 17 | -------------------------------------------------------------------------------- /html/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{pageTitle}} 9 | 59 | 60 | 61 |
62 |

{{pageTitle}}

63 |
64 |
65 | 66 |
67 |
68 | 69 |
70 | 71 |
用户名或密码错误
72 |
73 |
74 | 116 | 117 | ` 118 | -------------------------------------------------------------------------------- /html/upload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{pageTitle}} 9 | 145 | 146 | 147 |
148 |
149 |

{{pageTitle}}

150 | 进入管理页面 151 |
152 |
153 |

点击选择 或 拖拽文件到此处

154 | 155 |
156 |
157 |
158 | 159 |
160 |
161 | 162 | 163 | 164 |
165 | 171 |
172 |
173 |
174 | 175 | 362 | 363 | 364 | --------------------------------------------------------------------------------