├── 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 |
651 |
693 |
694 | `;
695 | }
696 |
697 | // 生成文件上传页面 /upload
698 | function generateUploadPage() {
699 | return `
700 |
701 |
702 |
703 |
704 |
705 |
706 | 文件上传
707 |
843 |
844 |
845 |
846 |
850 |
851 |
点击选择 或 拖拽文件到此处
852 |
853 |
854 |
855 |
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 |
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 |
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 |
73 |
74 |
116 |
117 | `
118 |
--------------------------------------------------------------------------------
/html/upload.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{pageTitle}}
9 |
145 |
146 |
147 |
148 |
152 |
153 |
点击选择 或 拖拽文件到此处
154 |
155 |
156 |
157 |
173 |
174 |
175 |
362 |
363 |
364 |
--------------------------------------------------------------------------------