Norcoo commited on
Commit
da7e3fb
·
verified ·
1 Parent(s): 1ff2c99

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +625 -632
  2. requirements.txt +0 -2
app.py CHANGED
@@ -1,632 +1,625 @@
1
- import gradio as gr
2
- import os
3
- import sqlite3
4
- import hashlib
5
- from datetime import datetime
6
- from pathlib import Path
7
- from PIL import Image
8
- import json
9
- import uuid
10
- import threading
11
- import time
12
- import random
13
- from fastapi import FastAPI
14
- from fastapi.responses import FileResponse, JSONResponse
15
- import uvicorn
16
-
17
- # 环境变量
18
- ACCESS_PASSWORD = os.environ.get("ACCESS_PASSWORD", "changeme")
19
- HF_USERNAME = os.environ.get("HF_USERNAME", "")
20
- HF_SPACE_NAME = os.environ.get("HF_SPACE_NAME", "")
21
-
22
- # 文件存储路径
23
- IMAGE_DIR = Path("uploaded_images")
24
- DB_PATH = "image_database.db"
25
-
26
- # 创建图片存储目录
27
- IMAGE_DIR.mkdir(exist_ok=True)
28
-
29
- def init_db():
30
- """初始化数据库"""
31
- try:
32
- conn = sqlite3.connect(DB_PATH)
33
- cursor = conn.cursor()
34
-
35
- # 创建图片表
36
- cursor.execute("""
37
- CREATE TABLE IF NOT EXISTS images (
38
- id INTEGER PRIMARY KEY AUTOINCREMENT,
39
- hash TEXT NOT NULL UNIQUE,
40
- filename TEXT NOT NULL,
41
- original_filename TEXT NOT NULL,
42
- file_path TEXT NOT NULL,
43
- file_size INTEGER,
44
- mime_type TEXT,
45
- upload_time TEXT NOT NULL,
46
- description TEXT
47
- )
48
- """)
49
-
50
- # 创建hash索引以提高查询速度
51
- cursor.execute("""
52
- CREATE INDEX IF NOT EXISTS idx_hash ON images(hash)
53
- """)
54
-
55
- conn.commit()
56
- conn.close()
57
- print("数据库初始化成功")
58
- return True
59
- except Exception as e:
60
- print(f"数据库初始化失败: {e}")
61
- return False
62
-
63
- def get_db_connection():
64
- """获取数据库连接"""
65
- conn = sqlite3.connect(DB_PATH)
66
- conn.row_factory = sqlite3.Row
67
- return conn
68
-
69
- def check_password(password):
70
- """验证密码"""
71
- return password == ACCESS_PASSWORD
72
-
73
- def generate_filename(original_filename):
74
- """生成唯一的文件名"""
75
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
76
- hash_suffix = hashlib.md5(str(datetime.now().timestamp()).encode()).hexdigest()[:8]
77
- ext = Path(original_filename).suffix.lower()
78
- if not ext:
79
- ext = ".png"
80
- return f"{timestamp}_{hash_suffix}{ext}"
81
-
82
- def generate_image_hash():
83
- """生成唯一的图片hash"""
84
- return uuid.uuid4().hex[:12]
85
-
86
- def generate_full_url(image_hash):
87
- """生成完整的图片URL(使用专用API端点)"""
88
- api_path = f"/api/img/{image_hash}"
89
- if HF_USERNAME and HF_SPACE_NAME:
90
- return f"https://{HF_USERNAME}-{HF_SPACE_NAME}.hf.space{api_path}"
91
- else:
92
- # 返回相对路径,用户访问时会自动补全域名
93
- return api_path
94
-
95
- def keep_alive():
96
- """防止系统休眠的后台线程"""
97
- while True:
98
- # 随机等待1-2分钟
99
- sleep_time = random.randint(60, 120)
100
- time.sleep(sleep_time)
101
- print(f"[Keep-Alive] {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
102
-
103
- def upload_images(images, description, password):
104
- """批量上传图片"""
105
- if not check_password(password):
106
- return "密码错误", None, ""
107
-
108
- if not images or len(images) == 0:
109
- return "请选择要上传的图片", None, ""
110
-
111
- results = []
112
- success_count = 0
113
- fail_count = 0
114
-
115
- try:
116
- for idx, image in enumerate(images):
117
- try:
118
- # 获取原始文件名
119
- if isinstance(image, str):
120
- # 如果是文件路径
121
- original_filename = Path(image).name
122
- img = Image.open(image)
123
- else:
124
- # 如果是 PIL Image
125
- original_filename = f"uploaded_image_{idx+1}.png"
126
- img = image
127
-
128
- # 生成新文件名和hash
129
- new_filename = generate_filename(original_filename)
130
- image_hash = generate_image_hash()
131
- file_path = IMAGE_DIR / new_filename
132
-
133
- # 保存图片
134
- img.save(file_path, quality=95, optimize=True)
135
-
136
- # 获取文件信息
137
- file_size = file_path.stat().st_size
138
- mime_type = f"image/{file_path.suffix[1:]}"
139
- upload_time = datetime.now().isoformat()
140
-
141
- # 插入数据库
142
- conn = get_db_connection()
143
- cursor = conn.cursor()
144
- cursor.execute(
145
- """
146
- INSERT INTO images (hash, filename, original_filename, file_path, file_size, mime_type, upload_time, description)
147
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
148
- """,
149
- (image_hash, new_filename, original_filename, str(file_path), file_size, mime_type, upload_time, description or "")
150
- )
151
- image_id = cursor.lastrowid
152
- conn.commit()
153
- conn.close()
154
-
155
- # 生成访问 URL
156
- full_url = generate_full_url(image_hash)
157
-
158
- results.append({
159
- 'hash': image_hash,
160
- 'filename': original_filename,
161
- 'size': file_size,
162
- 'url': full_url,
163
- 'status': 'success'
164
- })
165
- success_count += 1
166
-
167
- except Exception as e:
168
- results.append({
169
- 'filename': f"图片 {idx+1}",
170
- 'error': str(e),
171
- 'status': 'failed'
172
- })
173
- fail_count += 1
174
-
175
- # 生成结果文本
176
- result_text = f"## 上传结果\n\n"
177
- result_text += f"成功: {success_count} 张 | 失败: {fail_count} 张\n\n"
178
-
179
- for r in results:
180
- if r['status'] == 'success':
181
- result_text += f"### {r['filename']}\n"
182
- result_text += f"- **Hash**: {r['hash']}\n"
183
- result_text += f"- **大小**: {r['size'] / 1024:.2f} KB\n"
184
- result_text += f"- **URL**: `{r['url']}`\n\n"
185
- else:
186
- result_text += f"### {r['filename']}\n"
187
- result_text += f"- **错误**: {r['error']}\n\n"
188
-
189
- # 生成URL列表(用于复制)
190
- url_list = "\n".join([r['url'] for r in results if r['status'] == 'success'])
191
-
192
- return result_text, get_image_list_html(password), url_list
193
-
194
- except Exception as e:
195
- return f"上传失败: {str(e)}", None, ""
196
-
197
- def get_image_list_html(password):
198
- """获取图片列表(HTML格式)"""
199
- if not check_password(password):
200
- return "<p style='color: red;'>密码错误</p>"
201
-
202
- try:
203
- conn = get_db_connection()
204
- cursor = conn.cursor()
205
- cursor.execute(
206
- """
207
- SELECT id, hash, filename, original_filename, file_path, file_size, upload_time, description
208
- FROM images
209
- ORDER BY upload_time DESC
210
- """
211
- )
212
- rows = cursor.fetchall()
213
- conn.close()
214
-
215
- if not rows:
216
- return "<p>暂无图片</p>"
217
-
218
- # 生成HTML表格
219
- html = """
220
- <style>
221
- .image-table { width: 100%; border-collapse: collapse; margin-top: 10px; }
222
- .image-table th, .image-table td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
223
- .image-table th { background-color: #f0f0f0; font-weight: bold; }
224
- .image-table tr:hover { background-color: #f5f5f5; }
225
- .btn-copy { background: #2196F3; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; margin-right: 5px; }
226
- .btn-delete { background: #f44336; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; }
227
- .image-hash { font-family: monospace; color: #666; font-size: 12px; }
228
- </style>
229
- <table class="image-table">
230
- <thead>
231
- <tr>
232
- <th>Hash</th>
233
- <th>文件名</th>
234
- <th>大小</th>
235
- <th>上传时间</th>
236
- <th>描述</th>
237
- <th>操作</th>
238
- </tr>
239
- </thead>
240
- <tbody>
241
- """
242
-
243
- for row in rows:
244
- upload_time_str = datetime.fromisoformat(row['upload_time']).strftime('%Y-%m-%d %H:%M:%S')
245
- full_url = generate_full_url(row['hash'])
246
-
247
- html += f"""
248
- <tr id="row-{row['hash']}">
249
- <td class="image-hash">{row['hash']}</td>
250
- <td>{row['original_filename']}</td>
251
- <td>{row['file_size'] / 1024:.2f} KB</td>
252
- <td>{upload_time_str}</td>
253
- <td>{row['description'] or '-'}</td>
254
- <td>
255
- <button class="btn-copy" onclick="navigator.clipboard.writeText('{full_url}').then(() => alert('URL已复制'))">复制URL</button>
256
- <button class="btn-delete" data-hash="{row['hash']}" data-filename="{row['original_filename']}">删除</button>
257
- </td>
258
- </tr>
259
- """
260
-
261
- html += """
262
- </tbody>
263
- </table>
264
- """
265
-
266
- return html
267
-
268
- except Exception as e:
269
- return f"<p style='color: red;'>获取列表失败: {str(e)}</p>"
270
-
271
- def view_image_by_hash(image_hash, password):
272
- """通过hash查看图片"""
273
- if not check_password(password):
274
- return "密码错误", None, ""
275
-
276
- if not image_hash:
277
- return "请输入图片Hash", None, ""
278
-
279
- try:
280
- conn = get_db_connection()
281
- cursor = conn.cursor()
282
- cursor.execute(
283
- "SELECT * FROM images WHERE hash = ?",
284
- (image_hash,)
285
- )
286
- row = cursor.fetchone()
287
- conn.close()
288
-
289
- if not row:
290
- return "图片不存在", None, ""
291
-
292
- file_path = Path(row['file_path'])
293
-
294
- if not file_path.exists():
295
- return "图片文件已损坏或丢失", None, ""
296
-
297
- # 生成访问 URL
298
- full_url = generate_full_url(row['hash'])
299
-
300
- info_text = f"""## 图片信息
301
-
302
- **Hash**: {row['hash']}
303
- **原始文件名**: {row['original_filename']}
304
- **存储文件名**: {row['filename']}
305
- **大小**: {row['file_size'] / 1024:.2f} KB
306
- **上传时间**: {row['upload_time']}
307
- **描述**: {row['description'] or '无'}
308
-
309
- **访问URL**:
310
- ```
311
- {full_url}
312
- ```
313
- """
314
-
315
- return info_text, str(file_path), full_url
316
-
317
- except Exception as e:
318
- return f"查看失败: {str(e)}", None, ""
319
-
320
- def delete_image_by_hash(image_hash, password):
321
- """通过hash删除图片"""
322
- if not check_password(password):
323
- return "密码错误", None
324
-
325
- if not image_hash:
326
- return "请输入图片Hash", None
327
-
328
- try:
329
- conn = get_db_connection()
330
- cursor = conn.cursor()
331
-
332
- # 获取图片信息
333
- cursor.execute("SELECT * FROM images WHERE hash = ?", (image_hash,))
334
- row = cursor.fetchone()
335
-
336
- if not row:
337
- conn.close()
338
- return "图片不存在", None
339
-
340
- file_path = Path(row['file_path'])
341
- original_filename = row['original_filename']
342
-
343
- # 删除数据库记录
344
- cursor.execute("DELETE FROM images WHERE hash = ?", (image_hash,))
345
- conn.commit()
346
- conn.close()
347
-
348
- # 删除文件
349
- if file_path.exists():
350
- file_path.unlink()
351
-
352
- return f"已删除图片: {original_filename}", get_image_list_html(password)
353
-
354
- except Exception as e:
355
- return f"删除失败: {str(e)}", None
356
-
357
- def export_data(password):
358
- """导出数据(仅元数据)"""
359
- if not check_password(password):
360
- return "密码错误", None
361
-
362
- try:
363
- conn = get_db_connection()
364
- cursor = conn.cursor()
365
- cursor.execute("SELECT * FROM images ORDER BY upload_time DESC")
366
- rows = cursor.fetchall()
367
- conn.close()
368
-
369
- # 转换为 JSON
370
- data = []
371
- for row in rows:
372
- full_url = generate_full_url(row['hash'])
373
- data.append({
374
- 'hash': row['hash'],
375
- 'filename': row['filename'],
376
- 'original_filename': row['original_filename'],
377
- 'file_size': row['file_size'],
378
- 'upload_time': row['upload_time'],
379
- 'description': row['description'],
380
- 'url': full_url
381
- })
382
-
383
- # 保存为文件
384
- export_path = "image_metadata_export.json"
385
- with open(export_path, 'w', encoding='utf-8') as f:
386
- json.dump(data, f, ensure_ascii=False, indent=2)
387
-
388
- return f"元数据已导出到: {export_path}", export_path
389
-
390
- except Exception as e:
391
- return f"导出失败: {str(e)}", None
392
-
393
- # 初始化数据库
394
- init_success = init_db()
395
-
396
- # 启动防休眠线程
397
- keep_alive_thread = threading.Thread(target=keep_alive, daemon=True)
398
- keep_alive_thread.start()
399
-
400
- # 创建FastAPI应用
401
- app = FastAPI(title="私人图床服务")
402
-
403
- # 添加图片访问API路由(在Gradio之前)
404
- @app.get("/api/img/{image_hash}")
405
- async def get_image(image_hash: str):
406
- """通过hash获取图片"""
407
- try:
408
- conn = get_db_connection()
409
- cursor = conn.cursor()
410
- cursor.execute("SELECT * FROM images WHERE hash = ?", (image_hash,))
411
- row = cursor.fetchone()
412
- conn.close()
413
-
414
- if not row:
415
- return JSONResponse(
416
- status_code=404,
417
- content={"error": "Image not found"}
418
- )
419
-
420
- file_path = Path(row['file_path'])
421
-
422
- if not file_path.exists():
423
- return JSONResponse(
424
- status_code=404,
425
- content={"error": "Image file not found"}
426
- )
427
-
428
- # 返回图片文件
429
- return FileResponse(
430
- file_path,
431
- media_type=row['mime_type'],
432
- headers={
433
- "Cache-Control": "public, max-age=31536000",
434
- "Content-Disposition": f'inline; filename="{row["original_filename"]}"'
435
- }
436
- )
437
-
438
- except Exception as e:
439
- return JSONResponse(
440
- status_code=500,
441
- content={"error": str(e)}
442
- )
443
-
444
- @app.get("/api/img/{image_hash}/info")
445
- async def get_image_info(image_hash: str, password: str = None):
446
- """获取图片信息(需要密码)"""
447
- if not password or not check_password(password):
448
- return JSONResponse(
449
- status_code=401,
450
- content={"error": "Unauthorized"}
451
- )
452
-
453
- try:
454
- conn = get_db_connection()
455
- cursor = conn.cursor()
456
- cursor.execute("SELECT * FROM images WHERE hash = ?", (image_hash,))
457
- row = cursor.fetchone()
458
- conn.close()
459
-
460
- if not row:
461
- return JSONResponse(
462
- status_code=404,
463
- content={"error": "Image not found"}
464
- )
465
-
466
- return JSONResponse(content={
467
- "hash": row['hash'],
468
- "filename": row['filename'],
469
- "original_filename": row['original_filename'],
470
- "file_size": row['file_size'],
471
- "mime_type": row['mime_type'],
472
- "upload_time": row['upload_time'],
473
- "description": row['description'],
474
- "url": generate_full_url(row['hash'])
475
- })
476
-
477
- except Exception as e:
478
- return JSONResponse(
479
- status_code=500,
480
- content={"error": str(e)}
481
- )
482
-
483
- # 自定义CSS
484
- custom_css = """
485
- .compact-gallery .gallery {
486
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)) !important;
487
- }
488
- .url-box textarea {
489
- font-family: monospace;
490
- font-size: 12px;
491
- }
492
- """
493
-
494
- # 创建 Gradio 界面
495
- with gr.Blocks(title="My图床", theme=gr.themes.Soft(), css=custom_css) as gradio_app:
496
- gr.Markdown("# My图床服务")
497
-
498
- # 全局密码输入
499
- with gr.Row():
500
- global_password = gr.Textbox(
501
- label="访问密码",
502
- type="password",
503
- placeholder="输入密码以使用所有功能",
504
- scale=4
505
- )
506
-
507
- if not init_success:
508
- gr.Markdown("## 数据库初始化失败")
509
- else:
510
- with gr.Tabs():
511
- # 上传图片标签页
512
- with gr.Tab("上传图片"):
513
- with gr.Row():
514
- with gr.Column(scale=1):
515
- upload_images_input = gr.File(
516
- label="选择图片(支持多选)",
517
- file_count="multiple",
518
- file_types=["image"]
519
- )
520
- upload_desc_input = gr.Textbox(
521
- label="描述(可选)",
522
- placeholder="为这批图片添加描述...",
523
- lines=2
524
- )
525
- upload_btn = gr.Button("上传", variant="primary")
526
-
527
- with gr.Column(scale=1):
528
- upload_output = gr.Markdown(label="上传结果")
529
- upload_url_output = gr.Textbox(
530
- label="图片URL列表(可复制)",
531
- lines=5,
532
- elem_classes=["url-box"]
533
- )
534
-
535
- # 预览区域
536
- with gr.Accordion("图片预览", open=False):
537
- upload_preview = gr.Gallery(
538
- label="上传预览",
539
- show_label=False,
540
- columns=4,
541
- rows=2,
542
- height="auto",
543
- elem_classes=["compact-gallery"]
544
- )
545
-
546
- # 绑定事件
547
- upload_images_input.change(
548
- lambda files: [f.name for f in files] if files else [],
549
- inputs=[upload_images_input],
550
- outputs=[upload_preview]
551
- )
552
-
553
- upload_btn.click(
554
- lambda files, desc, pwd: upload_images([f.name for f in files] if files else [], desc, pwd),
555
- inputs=[upload_images_input, upload_desc_input, global_password],
556
- outputs=[upload_output, gr.HTML(visible=False), upload_url_output]
557
- )
558
-
559
- # 图片列表标签页
560
- with gr.Tab("图片列表"):
561
- list_refresh_btn = gr.Button("刷新列表", variant="primary")
562
- list_output = gr.HTML(label="图片列表")
563
-
564
- gr.Markdown("---")
565
- gr.Markdown("### 查看图片详情")
566
-
567
- with gr.Row():
568
- with gr.Column():
569
- view_hash_input = gr.Textbox(
570
- label="图片Hash",
571
- placeholder="输入图片Hash查看详情"
572
- )
573
- view_btn = gr.Button("查看详情", variant="secondary")
574
- view_info_output = gr.Markdown(label="图片信息")
575
- view_url_output = gr.Textbox(
576
- label="图片URL",
577
- lines=2,
578
- elem_classes=["url-box"]
579
- )
580
-
581
- with gr.Column():
582
- view_image_output = gr.Image(label="图片预览", height=400)
583
-
584
- # 删除图片区域
585
- gr.Markdown("---")
586
- gr.Markdown("### 删除图片")
587
- with gr.Row():
588
- delete_hash_input = gr.Textbox(
589
- label="图片Hash",
590
- placeholder="输入要删除的图片Hash"
591
- )
592
- delete_btn = gr.Button("删除", variant="stop")
593
- delete_output = gr.Textbox(label="删除结果", lines=2)
594
-
595
- # 绑定事件
596
- list_refresh_btn.click(
597
- get_image_list_html,
598
- inputs=[global_password],
599
- outputs=[list_output]
600
- )
601
-
602
- view_btn.click(
603
- view_image_by_hash,
604
- inputs=[view_hash_input, global_password],
605
- outputs=[view_info_output, view_image_output, view_url_output]
606
- )
607
-
608
- delete_btn.click(
609
- delete_image_by_hash,
610
- inputs=[delete_hash_input, global_password],
611
- outputs=[delete_output, list_output]
612
- )
613
-
614
- # 数据导出标签页
615
- with gr.Tab("导出数据"):
616
- gr.Markdown("导出所有图片的元数据(包含完整URL)为JSON格式")
617
-
618
- export_btn = gr.Button("导出元数据(JSON)", variant="primary")
619
- export_output = gr.Textbox(label="导出结果", lines=2)
620
- export_file_output = gr.File(label="下载文件")
621
-
622
- export_btn.click(
623
- export_data,
624
- inputs=[global_password],
625
- outputs=[export_output, export_file_output]
626
- )
627
-
628
- # 将Gradio应用挂载到FastAPI
629
- app = gr.mount_gradio_app(app, gradio_app, path="/")
630
-
631
- if __name__ == "__main__":
632
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ import gradio as gr
2
+ import os
3
+ import sqlite3
4
+ import hashlib
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from PIL import Image
8
+ import json
9
+ import uuid
10
+ import threading
11
+ import time
12
+ import random
13
+ from fastapi.responses import FileResponse, JSONResponse
14
+
15
+ # 环境变量
16
+ ACCESS_PASSWORD = os.environ.get("ACCESS_PASSWORD", "changeme")
17
+ HF_USERNAME = os.environ.get("HF_USERNAME", "")
18
+ HF_SPACE_NAME = os.environ.get("HF_SPACE_NAME", "")
19
+
20
+ # 文件存储路径
21
+ IMAGE_DIR = Path("uploaded_images")
22
+ DB_PATH = "image_database.db"
23
+
24
+ # 创建图片存储目录
25
+ IMAGE_DIR.mkdir(exist_ok=True)
26
+
27
+ def init_db():
28
+ """初始化数据库"""
29
+ try:
30
+ conn = sqlite3.connect(DB_PATH)
31
+ cursor = conn.cursor()
32
+
33
+ # 创建图片表
34
+ cursor.execute("""
35
+ CREATE TABLE IF NOT EXISTS images (
36
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
37
+ hash TEXT NOT NULL UNIQUE,
38
+ filename TEXT NOT NULL,
39
+ original_filename TEXT NOT NULL,
40
+ file_path TEXT NOT NULL,
41
+ file_size INTEGER,
42
+ mime_type TEXT,
43
+ upload_time TEXT NOT NULL,
44
+ description TEXT
45
+ )
46
+ """)
47
+
48
+ # 创建hash索引以提高查询速度
49
+ cursor.execute("""
50
+ CREATE INDEX IF NOT EXISTS idx_hash ON images(hash)
51
+ """)
52
+
53
+ conn.commit()
54
+ conn.close()
55
+ print("数据库初始化成功")
56
+ return True
57
+ except Exception as e:
58
+ print(f"数据库初始化失败: {e}")
59
+ return False
60
+
61
+ def get_db_connection():
62
+ """获取数据库连接"""
63
+ conn = sqlite3.connect(DB_PATH)
64
+ conn.row_factory = sqlite3.Row
65
+ return conn
66
+
67
+ def check_password(password):
68
+ """验证密码"""
69
+ return password == ACCESS_PASSWORD
70
+
71
+ def generate_filename(original_filename):
72
+ """生成唯一的文件名"""
73
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
74
+ hash_suffix = hashlib.md5(str(datetime.now().timestamp()).encode()).hexdigest()[:8]
75
+ ext = Path(original_filename).suffix.lower()
76
+ if not ext:
77
+ ext = ".png"
78
+ return f"{timestamp}_{hash_suffix}{ext}"
79
+
80
+ def generate_image_hash():
81
+ """生成唯一的图片hash"""
82
+ return uuid.uuid4().hex[:12]
83
+
84
+ def generate_full_url(image_hash):
85
+ """生成完整的图片URL(使用专用API端点)"""
86
+ api_path = f"/api/img/{image_hash}"
87
+ if HF_USERNAME and HF_SPACE_NAME:
88
+ return f"https://{HF_USERNAME}-{HF_SPACE_NAME}.hf.space{api_path}"
89
+ else:
90
+ # 返回相对路径,用户访问时会自动补全域名
91
+ return api_path
92
+
93
+ def keep_alive():
94
+ """防止系统休眠的后台线程"""
95
+ while True:
96
+ # 随机等待1-2分钟
97
+ sleep_time = random.randint(60, 120)
98
+ time.sleep(sleep_time)
99
+ print(f"[Keep-Alive] {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
100
+
101
+ def upload_images(images, description, password):
102
+ """批量上传图片"""
103
+ if not check_password(password):
104
+ return "密码错误", None, ""
105
+
106
+ if not images or len(images) == 0:
107
+ return "请选择要上传的图片", None, ""
108
+
109
+ results = []
110
+ success_count = 0
111
+ fail_count = 0
112
+
113
+ try:
114
+ for idx, image in enumerate(images):
115
+ try:
116
+ # 获取原始文件名
117
+ if isinstance(image, str):
118
+ # 如果是文件路径
119
+ original_filename = Path(image).name
120
+ img = Image.open(image)
121
+ else:
122
+ # 如果是 PIL Image
123
+ original_filename = f"uploaded_image_{idx+1}.png"
124
+ img = image
125
+
126
+ # 生成新文件名和hash
127
+ new_filename = generate_filename(original_filename)
128
+ image_hash = generate_image_hash()
129
+ file_path = IMAGE_DIR / new_filename
130
+
131
+ # 保存图片
132
+ img.save(file_path, quality=95, optimize=True)
133
+
134
+ # 获取文件信息
135
+ file_size = file_path.stat().st_size
136
+ mime_type = f"image/{file_path.suffix[1:]}"
137
+ upload_time = datetime.now().isoformat()
138
+
139
+ # 插入数据库
140
+ conn = get_db_connection()
141
+ cursor = conn.cursor()
142
+ cursor.execute(
143
+ """
144
+ INSERT INTO images (hash, filename, original_filename, file_path, file_size, mime_type, upload_time, description)
145
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
146
+ """,
147
+ (image_hash, new_filename, original_filename, str(file_path), file_size, mime_type, upload_time, description or "")
148
+ )
149
+ image_id = cursor.lastrowid
150
+ conn.commit()
151
+ conn.close()
152
+
153
+ # 生成访问 URL
154
+ full_url = generate_full_url(image_hash)
155
+
156
+ results.append({
157
+ 'hash': image_hash,
158
+ 'filename': original_filename,
159
+ 'size': file_size,
160
+ 'url': full_url,
161
+ 'status': 'success'
162
+ })
163
+ success_count += 1
164
+
165
+ except Exception as e:
166
+ results.append({
167
+ 'filename': f"图片 {idx+1}",
168
+ 'error': str(e),
169
+ 'status': 'failed'
170
+ })
171
+ fail_count += 1
172
+
173
+ # 生成结果文本
174
+ result_text = f"## 上传结果\n\n"
175
+ result_text += f"成功: {success_count} 张 | 失败: {fail_count} 张\n\n"
176
+
177
+ for r in results:
178
+ if r['status'] == 'success':
179
+ result_text += f"### {r['filename']}\n"
180
+ result_text += f"- **Hash**: {r['hash']}\n"
181
+ result_text += f"- **大小**: {r['size'] / 1024:.2f} KB\n"
182
+ result_text += f"- **URL**: `{r['url']}`\n\n"
183
+ else:
184
+ result_text += f"### {r['filename']}\n"
185
+ result_text += f"- **错误**: {r['error']}\n\n"
186
+
187
+ # 生成URL列表(用于复制)
188
+ url_list = "\n".join([r['url'] for r in results if r['status'] == 'success'])
189
+
190
+ return result_text, get_image_list_html(password), url_list
191
+
192
+ except Exception as e:
193
+ return f"上传失败: {str(e)}", None, ""
194
+
195
+ def get_image_list_html(password):
196
+ """获取图片列表(HTML格式)"""
197
+ if not check_password(password):
198
+ return "<p style='color: red;'>密码错误</p>"
199
+
200
+ try:
201
+ conn = get_db_connection()
202
+ cursor = conn.cursor()
203
+ cursor.execute(
204
+ """
205
+ SELECT id, hash, filename, original_filename, file_path, file_size, upload_time, description
206
+ FROM images
207
+ ORDER BY upload_time DESC
208
+ """
209
+ )
210
+ rows = cursor.fetchall()
211
+ conn.close()
212
+
213
+ if not rows:
214
+ return "<p>暂无图片</p>"
215
+
216
+ # 生成HTML表格
217
+ html = """
218
+ <style>
219
+ .image-table { width: 100%; border-collapse: collapse; margin-top: 10px; }
220
+ .image-table th, .image-table td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
221
+ .image-table th { background-color: #f0f0f0; font-weight: bold; }
222
+ .image-table tr:hover { background-color: #f5f5f5; }
223
+ .btn-copy { background: #2196F3; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; margin-right: 5px; }
224
+ .btn-delete { background: #f44336; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; }
225
+ .image-hash { font-family: monospace; color: #666; font-size: 12px; }
226
+ </style>
227
+ <table class="image-table">
228
+ <thead>
229
+ <tr>
230
+ <th>Hash</th>
231
+ <th>文件名</th>
232
+ <th>大小</th>
233
+ <th>上传时间</th>
234
+ <th>描述</th>
235
+ <th>操作</th>
236
+ </tr>
237
+ </thead>
238
+ <tbody>
239
+ """
240
+
241
+ for row in rows:
242
+ upload_time_str = datetime.fromisoformat(row['upload_time']).strftime('%Y-%m-%d %H:%M:%S')
243
+ full_url = generate_full_url(row['hash'])
244
+
245
+ html += f"""
246
+ <tr id="row-{row['hash']}">
247
+ <td class="image-hash">{row['hash']}</td>
248
+ <td>{row['original_filename']}</td>
249
+ <td>{row['file_size'] / 1024:.2f} KB</td>
250
+ <td>{upload_time_str}</td>
251
+ <td>{row['description'] or '-'}</td>
252
+ <td>
253
+ <button class="btn-copy" onclick="navigator.clipboard.writeText('{full_url}').then(() => alert('URL已复制'))">复制URL</button>
254
+ <button class="btn-delete" data-hash="{row['hash']}" data-filename="{row['original_filename']}">删除</button>
255
+ </td>
256
+ </tr>
257
+ """
258
+
259
+ html += """
260
+ </tbody>
261
+ </table>
262
+ """
263
+
264
+ return html
265
+
266
+ except Exception as e:
267
+ return f"<p style='color: red;'>获取列表失败: {str(e)}</p>"
268
+
269
+ def view_image_by_hash(image_hash, password):
270
+ """通过hash查看图片"""
271
+ if not check_password(password):
272
+ return "密码错误", None, ""
273
+
274
+ if not image_hash:
275
+ return "请输入图片Hash", None, ""
276
+
277
+ try:
278
+ conn = get_db_connection()
279
+ cursor = conn.cursor()
280
+ cursor.execute(
281
+ "SELECT * FROM images WHERE hash = ?",
282
+ (image_hash,)
283
+ )
284
+ row = cursor.fetchone()
285
+ conn.close()
286
+
287
+ if not row:
288
+ return "图片不存在", None, ""
289
+
290
+ file_path = Path(row['file_path'])
291
+
292
+ if not file_path.exists():
293
+ return "图片文件已损坏或丢失", None, ""
294
+
295
+ # 生成访问 URL
296
+ full_url = generate_full_url(row['hash'])
297
+
298
+ info_text = f"""## 图片信息
299
+
300
+ **Hash**: {row['hash']}
301
+ **原始文件名**: {row['original_filename']}
302
+ **存储文件名**: {row['filename']}
303
+ **大小**: {row['file_size'] / 1024:.2f} KB
304
+ **上传时间**: {row['upload_time']}
305
+ **描述**: {row['description'] or '无'}
306
+
307
+ **访问URL**:
308
+ ```
309
+ {full_url}
310
+ ```
311
+ """
312
+
313
+ return info_text, str(file_path), full_url
314
+
315
+ except Exception as e:
316
+ return f"查看失败: {str(e)}", None, ""
317
+
318
+ def delete_image_by_hash(image_hash, password):
319
+ """通过hash删除图片"""
320
+ if not check_password(password):
321
+ return "密码错误", None
322
+
323
+ if not image_hash:
324
+ return "请输入图片Hash", None
325
+
326
+ try:
327
+ conn = get_db_connection()
328
+ cursor = conn.cursor()
329
+
330
+ # 获取图片信息
331
+ cursor.execute("SELECT * FROM images WHERE hash = ?", (image_hash,))
332
+ row = cursor.fetchone()
333
+
334
+ if not row:
335
+ conn.close()
336
+ return "图片不存在", None
337
+
338
+ file_path = Path(row['file_path'])
339
+ original_filename = row['original_filename']
340
+
341
+ # 删除数据库记录
342
+ cursor.execute("DELETE FROM images WHERE hash = ?", (image_hash,))
343
+ conn.commit()
344
+ conn.close()
345
+
346
+ # 删除文件
347
+ if file_path.exists():
348
+ file_path.unlink()
349
+
350
+ return f"已删除图片: {original_filename}", get_image_list_html(password)
351
+
352
+ except Exception as e:
353
+ return f"删除失败: {str(e)}", None
354
+
355
+ def export_data(password):
356
+ """导出数据(仅元数据)"""
357
+ if not check_password(password):
358
+ return "密码错误", None
359
+
360
+ try:
361
+ conn = get_db_connection()
362
+ cursor = conn.cursor()
363
+ cursor.execute("SELECT * FROM images ORDER BY upload_time DESC")
364
+ rows = cursor.fetchall()
365
+ conn.close()
366
+
367
+ # 转换为 JSON
368
+ data = []
369
+ for row in rows:
370
+ full_url = generate_full_url(row['hash'])
371
+ data.append({
372
+ 'hash': row['hash'],
373
+ 'filename': row['filename'],
374
+ 'original_filename': row['original_filename'],
375
+ 'file_size': row['file_size'],
376
+ 'upload_time': row['upload_time'],
377
+ 'description': row['description'],
378
+ 'url': full_url
379
+ })
380
+
381
+ # 保存为文件
382
+ export_path = "image_metadata_export.json"
383
+ with open(export_path, 'w', encoding='utf-8') as f:
384
+ json.dump(data, f, ensure_ascii=False, indent=2)
385
+
386
+ return f"元数据已导出到: {export_path}", export_path
387
+
388
+ except Exception as e:
389
+ return f"导出失败: {str(e)}", None
390
+
391
+ # 初始化数据库
392
+ init_success = init_db()
393
+
394
+ # 启动防休眠线程
395
+ keep_alive_thread = threading.Thread(target=keep_alive, daemon=True)
396
+ keep_alive_thread.start()
397
+
398
+ # 自定义CSS
399
+ custom_css = """
400
+ .compact-gallery .gallery {
401
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)) !important;
402
+ }
403
+ .url-box textarea {
404
+ font-family: monospace;
405
+ font-size: 12px;
406
+ }
407
+ """
408
+
409
+ # 创建 Gradio 界面
410
+ with gr.Blocks(title="My图床", theme=gr.themes.Soft(), css=custom_css) as gradio_app:
411
+ gr.Markdown("# My图床服务")
412
+
413
+ # 全局密码输入
414
+ with gr.Row():
415
+ global_password = gr.Textbox(
416
+ label="访问密码",
417
+ type="password",
418
+ placeholder="输入密码以使用所有功能",
419
+ scale=4
420
+ )
421
+
422
+ if not init_success:
423
+ gr.Markdown("## 数据库初始化失败")
424
+ else:
425
+ with gr.Tabs():
426
+ # 上传图片标签页
427
+ with gr.Tab("上传图片"):
428
+ with gr.Row():
429
+ with gr.Column(scale=1):
430
+ upload_images_input = gr.File(
431
+ label="选择图片(支持多选)",
432
+ file_count="multiple",
433
+ file_types=["image"]
434
+ )
435
+ upload_desc_input = gr.Textbox(
436
+ label="描述(可选)",
437
+ placeholder="为这批图片添加描述...",
438
+ lines=2
439
+ )
440
+ upload_btn = gr.Button("上传", variant="primary")
441
+
442
+ with gr.Column(scale=1):
443
+ upload_output = gr.Markdown(label="上传结果")
444
+ upload_url_output = gr.Textbox(
445
+ label="图片URL列表(可复制)",
446
+ lines=5,
447
+ elem_classes=["url-box"]
448
+ )
449
+
450
+ # 预览区域
451
+ with gr.Accordion("图片预览", open=False):
452
+ upload_preview = gr.Gallery(
453
+ label="上传预览",
454
+ show_label=False,
455
+ columns=4,
456
+ rows=2,
457
+ height="auto",
458
+ elem_classes=["compact-gallery"]
459
+ )
460
+
461
+ # 绑定事件
462
+ upload_images_input.change(
463
+ lambda files: [f.name for f in files] if files else [],
464
+ inputs=[upload_images_input],
465
+ outputs=[upload_preview]
466
+ )
467
+
468
+ upload_btn.click(
469
+ lambda files, desc, pwd: upload_images([f.name for f in files] if files else [], desc, pwd),
470
+ inputs=[upload_images_input, upload_desc_input, global_password],
471
+ outputs=[upload_output, gr.HTML(visible=False), upload_url_output]
472
+ )
473
+
474
+ # 图片列表标签页
475
+ with gr.Tab("图片列表"):
476
+ list_refresh_btn = gr.Button("刷新列表", variant="primary")
477
+ list_output = gr.HTML(label="图片列表")
478
+
479
+ gr.Markdown("---")
480
+ gr.Markdown("### 查看图片详情")
481
+
482
+ with gr.Row():
483
+ with gr.Column():
484
+ view_hash_input = gr.Textbox(
485
+ label="图片Hash",
486
+ placeholder="输入图片Hash查看详情"
487
+ )
488
+ view_btn = gr.Button("查看详情", variant="secondary")
489
+ view_info_output = gr.Markdown(label="图片信息")
490
+ view_url_output = gr.Textbox(
491
+ label="图片URL",
492
+ lines=2,
493
+ elem_classes=["url-box"]
494
+ )
495
+
496
+ with gr.Column():
497
+ view_image_output = gr.Image(label="图片预览", height=400)
498
+
499
+ # 删除图片区域
500
+ gr.Markdown("---")
501
+ gr.Markdown("### 删除图片")
502
+ with gr.Row():
503
+ delete_hash_input = gr.Textbox(
504
+ label="图片Hash",
505
+ placeholder="输入要删除的图片Hash"
506
+ )
507
+ delete_btn = gr.Button("删除", variant="stop")
508
+ delete_output = gr.Textbox(label="删除结果", lines=2)
509
+
510
+ # 绑定事件
511
+ list_refresh_btn.click(
512
+ get_image_list_html,
513
+ inputs=[global_password],
514
+ outputs=[list_output]
515
+ )
516
+
517
+ view_btn.click(
518
+ view_image_by_hash,
519
+ inputs=[view_hash_input, global_password],
520
+ outputs=[view_info_output, view_image_output, view_url_output]
521
+ )
522
+
523
+ delete_btn.click(
524
+ delete_image_by_hash,
525
+ inputs=[delete_hash_input, global_password],
526
+ outputs=[delete_output, list_output]
527
+ )
528
+
529
+ # 数据导出标签页
530
+ with gr.Tab("导出数据"):
531
+ gr.Markdown("导出所有图片的元数据(包含完整URL)为JSON格式")
532
+
533
+ export_btn = gr.Button("导出元数据(JSON)", variant="primary")
534
+ export_output = gr.Textbox(label="导出结果", lines=2)
535
+ export_file_output = gr.File(label="下载文件")
536
+
537
+ export_btn.click(
538
+ export_data,
539
+ inputs=[global_password],
540
+ outputs=[export_output, export_file_output]
541
+ )
542
+
543
+ # 在Gradio的底层FastAPI上添加自定义API路由
544
+ @gradio_app.fastapi_app.get("/api/img/{image_hash}")
545
+ async def get_image(image_hash: str):
546
+ """通过hash获取图片"""
547
+ try:
548
+ conn = get_db_connection()
549
+ cursor = conn.cursor()
550
+ cursor.execute("SELECT * FROM images WHERE hash = ?", (image_hash,))
551
+ row = cursor.fetchone()
552
+ conn.close()
553
+
554
+ if not row:
555
+ return JSONResponse(
556
+ status_code=404,
557
+ content={"error": "Image not found"}
558
+ )
559
+
560
+ file_path = Path(row['file_path'])
561
+
562
+ if not file_path.exists():
563
+ return JSONResponse(
564
+ status_code=404,
565
+ content={"error": "Image file not found"}
566
+ )
567
+
568
+ # 返回图片文件
569
+ return FileResponse(
570
+ file_path,
571
+ media_type=row['mime_type'],
572
+ headers={
573
+ "Cache-Control": "public, max-age=31536000",
574
+ "Content-Disposition": f'inline; filename="{row["original_filename"]}"'
575
+ }
576
+ )
577
+
578
+ except Exception as e:
579
+ return JSONResponse(
580
+ status_code=500,
581
+ content={"error": str(e)}
582
+ )
583
+
584
+ @gradio_app.fastapi_app.get("/api/img/{image_hash}/info")
585
+ async def get_image_info(image_hash: str, password: str = None):
586
+ """获取图片信息(需要密码)"""
587
+ if not password or not check_password(password):
588
+ return JSONResponse(
589
+ status_code=401,
590
+ content={"error": "Unauthorized"}
591
+ )
592
+
593
+ try:
594
+ conn = get_db_connection()
595
+ cursor = conn.cursor()
596
+ cursor.execute("SELECT * FROM images WHERE hash = ?", (image_hash,))
597
+ row = cursor.fetchone()
598
+ conn.close()
599
+
600
+ if not row:
601
+ return JSONResponse(
602
+ status_code=404,
603
+ content={"error": "Image not found"}
604
+ )
605
+
606
+ return JSONResponse(content={
607
+ "hash": row['hash'],
608
+ "filename": row['filename'],
609
+ "original_filename": row['original_filename'],
610
+ "file_size": row['file_size'],
611
+ "mime_type": row['mime_type'],
612
+ "upload_time": row['upload_time'],
613
+ "description": row['description'],
614
+ "url": generate_full_url(row['hash'])
615
+ })
616
+
617
+ except Exception as e:
618
+ return JSONResponse(
619
+ status_code=500,
620
+ content={"error": str(e)}
621
+ )
622
+
623
+ if __name__ == "__main__":
624
+ # 直接启动Gradio,它会使用自己的FastAPI实例
625
+ gradio_app.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,5 +1,3 @@
1
  gradio==4.44.0
2
  Pillow==10.4.0
3
- fastapi>=0.104.0
4
- uvicorn[standard]>=0.24.0
5
 
 
1
  gradio==4.44.0
2
  Pillow==10.4.0
 
 
3