Norcoo commited on
Commit
ea08e0a
·
verified ·
1 Parent(s): 36ce68c

Upload 2 files

Browse files
Files changed (1) hide show
  1. app.py +139 -219
app.py CHANGED
@@ -6,6 +6,10 @@ from datetime import datetime
6
  from pathlib import Path
7
  from PIL import Image
8
  import json
 
 
 
 
9
  from fastapi import FastAPI, Response
10
  from fastapi.responses import FileResponse, JSONResponse
11
 
@@ -31,6 +35,7 @@ def init_db():
31
  cursor.execute("""
32
  CREATE TABLE IF NOT EXISTS images (
33
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 
34
  filename TEXT NOT NULL,
35
  original_filename TEXT NOT NULL,
36
  file_path TEXT NOT NULL,
@@ -41,12 +46,17 @@ def init_db():
41
  )
42
  """)
43
 
 
 
 
 
 
44
  conn.commit()
45
  conn.close()
46
- print("数据库初始化成功")
47
  return True
48
  except Exception as e:
49
- print(f"数据库初始化失败: {e}")
50
  return False
51
 
52
  def get_db_connection():
@@ -68,22 +78,34 @@ def generate_filename(original_filename):
68
  ext = ".png"
69
  return f"{timestamp}_{hash_suffix}{ext}"
70
 
71
- def generate_full_url(image_id):
 
 
 
 
72
  """生成完整的图片URL(使用专用API端点)"""
73
- api_path = f"/api/img/{image_id}"
74
  if HF_USERNAME and HF_SPACE_NAME:
75
  return f"https://{HF_USERNAME}-{HF_SPACE_NAME}.hf.space{api_path}"
76
  else:
77
  # 返回相对路径,用户访问时会自动补全域名
78
  return api_path
79
 
 
 
 
 
 
 
 
 
80
  def upload_images(images, description, password):
81
  """批量上传图片"""
82
  if not check_password(password):
83
- return "❌ 密码错误!", None, ""
84
 
85
  if not images or len(images) == 0:
86
- return "❌ 请选择要上传的图片!", None, ""
87
 
88
  results = []
89
  success_count = 0
@@ -102,8 +124,9 @@ def upload_images(images, description, password):
102
  original_filename = f"uploaded_image_{idx+1}.png"
103
  img = image
104
 
105
- # 生成新文件名
106
  new_filename = generate_filename(original_filename)
 
107
  file_path = IMAGE_DIR / new_filename
108
 
109
  # 保存图片
@@ -119,20 +142,20 @@ def upload_images(images, description, password):
119
  cursor = conn.cursor()
120
  cursor.execute(
121
  """
122
- INSERT INTO images (filename, original_filename, file_path, file_size, mime_type, upload_time, description)
123
- VALUES (?, ?, ?, ?, ?, ?, ?)
124
  """,
125
- (new_filename, original_filename, str(file_path), file_size, mime_type, upload_time, description or "")
126
  )
127
  image_id = cursor.lastrowid
128
  conn.commit()
129
  conn.close()
130
 
131
  # 生成访问 URL
132
- full_url = generate_full_url(image_id)
133
 
134
  results.append({
135
- 'id': image_id,
136
  'filename': original_filename,
137
  'size': file_size,
138
  'url': full_url,
@@ -149,17 +172,17 @@ def upload_images(images, description, password):
149
  fail_count += 1
150
 
151
  # 生成结果文本
152
- result_text = f"## 📤 上传结果\n\n"
153
- result_text += f"成功: {success_count} 张 | 失败: {fail_count} 张\n\n"
154
 
155
  for r in results:
156
  if r['status'] == 'success':
157
- result_text += f"### {r['filename']}\n"
158
- result_text += f"- **ID**: {r['id']}\n"
159
  result_text += f"- **大小**: {r['size'] / 1024:.2f} KB\n"
160
  result_text += f"- **URL**: `{r['url']}`\n\n"
161
  else:
162
- result_text += f"### {r['filename']}\n"
163
  result_text += f"- **错误**: {r['error']}\n\n"
164
 
165
  # 生成URL列表(用于复制)
@@ -168,19 +191,19 @@ def upload_images(images, description, password):
168
  return result_text, get_image_list_html(password), url_list
169
 
170
  except Exception as e:
171
- return f"上传失败: {str(e)}", None, ""
172
 
173
  def get_image_list_html(password):
174
  """获取图片列表(HTML格式)"""
175
  if not check_password(password):
176
- return "<p style='color: red;'>❌ 密码错误!</p>"
177
 
178
  try:
179
  conn = get_db_connection()
180
  cursor = conn.cursor()
181
  cursor.execute(
182
  """
183
- SELECT id, filename, original_filename, file_path, file_size, upload_time, description
184
  FROM images
185
  ORDER BY upload_time DESC
186
  """
@@ -198,15 +221,14 @@ def get_image_list_html(password):
198
  .image-table th, .image-table td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
199
  .image-table th { background-color: #f0f0f0; font-weight: bold; }
200
  .image-table tr:hover { background-color: #f5f5f5; }
201
- .btn-view { background: #4CAF50; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; margin-right: 5px; }
202
  .btn-copy { background: #2196F3; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; margin-right: 5px; }
203
  .btn-delete { background: #f44336; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; }
204
- .image-id { font-weight: bold; color: #2196F3; }
205
  </style>
206
  <table class="image-table">
207
  <thead>
208
  <tr>
209
- <th>ID</th>
210
  <th>文件名</th>
211
  <th>大小</th>
212
  <th>上传时间</th>
@@ -219,18 +241,18 @@ def get_image_list_html(password):
219
 
220
  for row in rows:
221
  upload_time_str = datetime.fromisoformat(row['upload_time']).strftime('%Y-%m-%d %H:%M:%S')
222
- full_url = generate_full_url(row['id'])
223
 
224
  html += f"""
225
- <tr>
226
- <td class="image-id">{row['id']}</td>
227
  <td>{row['original_filename']}</td>
228
  <td>{row['file_size'] / 1024:.2f} KB</td>
229
  <td>{upload_time_str}</td>
230
  <td>{row['description'] or '-'}</td>
231
  <td>
232
- <button class="btn-view" onclick="window.viewImage_{row['id']}()">查看</button>
233
- <button class="btn-copy" onclick="navigator.clipboard.writeText('{full_url}').then(() => alert('URL已复制到剪贴板!'))">复制URL</button>
234
  </td>
235
  </tr>
236
  """
@@ -243,40 +265,40 @@ def get_image_list_html(password):
243
  return html
244
 
245
  except Exception as e:
246
- return f"<p style='color: red;'>❌ 获取列表失败: {str(e)}</p>"
247
 
248
- def view_image_by_id(image_id, password):
249
- """通过ID查看图片"""
250
  if not check_password(password):
251
- return "❌ 密码错误!", None, ""
252
 
253
- if not image_id:
254
- return "请输入图片ID!", None, ""
255
 
256
  try:
257
  conn = get_db_connection()
258
  cursor = conn.cursor()
259
  cursor.execute(
260
- "SELECT * FROM images WHERE id = ?",
261
- (int(image_id),)
262
  )
263
  row = cursor.fetchone()
264
  conn.close()
265
 
266
  if not row:
267
- return "❌ 图片不存在!", None, ""
268
 
269
  file_path = Path(row['file_path'])
270
 
271
  if not file_path.exists():
272
- return "❌ 图片文件已损坏或丢失!", None, ""
273
 
274
  # 生成访问 URL
275
- full_url = generate_full_url(row['id'])
276
 
277
- info_text = f"""## 📷 图片信息
278
 
279
- **ID**: {row['id']}
280
  **原始文件名**: {row['original_filename']}
281
  **存储文件名**: {row['filename']}
282
  **大小**: {row['file_size'] / 1024:.2f} KB
@@ -292,33 +314,33 @@ def view_image_by_id(image_id, password):
292
  return info_text, str(file_path), full_url
293
 
294
  except Exception as e:
295
- return f"查看失败: {str(e)}", None, ""
296
 
297
- def delete_image(image_id, password):
298
- """删除图片"""
299
  if not check_password(password):
300
- return "❌ 密码错误!", None
301
 
302
- if not image_id:
303
- return "请输入图片ID!", None
304
 
305
  try:
306
  conn = get_db_connection()
307
  cursor = conn.cursor()
308
 
309
  # 获取图片信息
310
- cursor.execute("SELECT * FROM images WHERE id = ?", (int(image_id),))
311
  row = cursor.fetchone()
312
 
313
  if not row:
314
  conn.close()
315
- return "❌ 图片不存在!", None
316
 
317
  file_path = Path(row['file_path'])
318
  original_filename = row['original_filename']
319
 
320
  # 删除数据库记录
321
- cursor.execute("DELETE FROM images WHERE id = ?", (int(image_id),))
322
  conn.commit()
323
  conn.close()
324
 
@@ -326,62 +348,15 @@ def delete_image(image_id, password):
326
  if file_path.exists():
327
  file_path.unlink()
328
 
329
- return f"已删除图片: {original_filename} (ID: {image_id})", get_image_list_html(password)
330
-
331
- except Exception as e:
332
- return f"❌ 删除失败: {str(e)}", None
333
-
334
- def get_stats(password):
335
- """获取统计信息"""
336
- if not check_password(password):
337
- return "❌ 密码错误!"
338
-
339
- try:
340
- conn = get_db_connection()
341
- cursor = conn.cursor()
342
-
343
- # 总图片数
344
- cursor.execute("SELECT COUNT(*) as count FROM images")
345
- total_count = cursor.fetchone()['count']
346
-
347
- # 总大小
348
- cursor.execute("SELECT SUM(file_size) as total_size FROM images")
349
- total_size = cursor.fetchone()['total_size'] or 0
350
-
351
- # 最近上传
352
- cursor.execute("SELECT upload_time FROM images ORDER BY upload_time DESC LIMIT 1")
353
- last_upload = cursor.fetchone()
354
-
355
- conn.close()
356
-
357
- # 检查配置状态
358
- config_status = "✅ 已配置" if (HF_USERNAME and HF_SPACE_NAME) else "⚠️ 未配置(仅显示相对路径)"
359
-
360
- stats = f"""## 📊 统计信息
361
-
362
- **总图片数**: {total_count} 张
363
- **总存储大小**: {total_size / 1024 / 1024:.2f} MB
364
- **最近上传**: {last_upload['upload_time'] if last_upload else '暂无'}
365
-
366
- ### 🔧 配置信息
367
- **HF用户名**: {HF_USERNAME or '未设置'}
368
- **Space名称**: {HF_SPACE_NAME or '未设置'}
369
- **URL生成**: {config_status}
370
-
371
- ### 📁 存储信息
372
- **图片目录**: {IMAGE_DIR.absolute()}
373
- **数据库**: {Path(DB_PATH).absolute()}
374
- """
375
-
376
- return stats
377
 
378
  except Exception as e:
379
- return f" 获取统计失败: {str(e)}"
380
 
381
  def export_data(password):
382
  """导出数据(仅元数据)"""
383
  if not check_password(password):
384
- return "❌ 密码错误!", None
385
 
386
  try:
387
  conn = get_db_connection()
@@ -393,9 +368,9 @@ def export_data(password):
393
  # 转换为 JSON
394
  data = []
395
  for row in rows:
396
- full_url = generate_full_url(row['id'])
397
  data.append({
398
- 'id': row['id'],
399
  'filename': row['filename'],
400
  'original_filename': row['original_filename'],
401
  'file_size': row['file_size'],
@@ -409,25 +384,29 @@ def export_data(password):
409
  with open(export_path, 'w', encoding='utf-8') as f:
410
  json.dump(data, f, ensure_ascii=False, indent=2)
411
 
412
- return f"元数据已导出到: {export_path}", export_path
413
 
414
  except Exception as e:
415
- return f"导出失败: {str(e)}", None
416
 
417
  # 初始化数据库
418
  init_success = init_db()
419
 
 
 
 
 
420
  # 创建FastAPI app
421
  fastapi_app = FastAPI()
422
 
423
  # 添加图片访问API端点
424
- @fastapi_app.get("/api/img/{image_id}")
425
- async def get_image(image_id: int):
426
- """通过ID获取图片"""
427
  try:
428
  conn = get_db_connection()
429
  cursor = conn.cursor()
430
- cursor.execute("SELECT * FROM images WHERE id = ?", (image_id,))
431
  row = cursor.fetchone()
432
  conn.close()
433
 
@@ -462,8 +441,8 @@ async def get_image(image_id: int):
462
  )
463
 
464
  # 添加图片信息API端点(可选,用于获取元数据)
465
- @fastapi_app.get("/api/img/{image_id}/info")
466
- async def get_image_info(image_id: int, password: str = None):
467
  """获取图片信息(需要密码)"""
468
  if not password or not check_password(password):
469
  return JSONResponse(
@@ -474,7 +453,7 @@ async def get_image_info(image_id: int, password: str = None):
474
  try:
475
  conn = get_db_connection()
476
  cursor = conn.cursor()
477
- cursor.execute("SELECT * FROM images WHERE id = ?", (image_id,))
478
  row = cursor.fetchone()
479
  conn.close()
480
 
@@ -485,14 +464,14 @@ async def get_image_info(image_id: int, password: str = None):
485
  )
486
 
487
  return JSONResponse(content={
488
- "id": row['id'],
489
  "filename": row['filename'],
490
  "original_filename": row['original_filename'],
491
  "file_size": row['file_size'],
492
  "mime_type": row['mime_type'],
493
  "upload_time": row['upload_time'],
494
  "description": row['description'],
495
- "url": generate_full_url(row['id'])
496
  })
497
 
498
  except Exception as e:
@@ -513,25 +492,26 @@ custom_css = """
513
  """
514
 
515
  # 创建 Gradio 界面
516
- with gr.Blocks(title="私人图床服务", theme=gr.themes.Soft(), css=custom_css) as gradio_app:
517
- gr.Markdown("# 🖼️ 私人图床服务")
518
- gr.Markdown("⚠️ 请输入密码以使用此服务 | 支持批量上传 | 图片存储在服务器本地")
 
 
 
 
 
 
 
 
519
 
520
  if not init_success:
521
- gr.Markdown("## ❌ 数据库初始化失败!")
522
  else:
523
  with gr.Tabs():
524
  # 上传图片标签页
525
- with gr.Tab("📤 上传图片"):
526
- gr.Markdown("### 批量上传图片(支持多张)")
527
-
528
  with gr.Row():
529
  with gr.Column(scale=1):
530
- upload_password_input = gr.Textbox(
531
- label="🔐 密码",
532
- type="password",
533
- placeholder="输入访问密码"
534
- )
535
  upload_images_input = gr.File(
536
  label="选择图片(支持多选)",
537
  file_count="multiple",
@@ -542,21 +522,18 @@ with gr.Blocks(title="私人图床服务", theme=gr.themes.Soft(), css=custom_cs
542
  placeholder="为这批图片添加描述...",
543
  lines=2
544
  )
545
-
546
- with gr.Row():
547
- upload_btn = gr.Button("📤 上传", variant="primary", scale=2)
548
- toggle_preview_btn = gr.Button("👁️ 显示/隐藏预览", scale=1)
549
 
550
  with gr.Column(scale=1):
551
  upload_output = gr.Markdown(label="上传结果")
552
  upload_url_output = gr.Textbox(
553
- label="📋 图片URL列表(可复制)",
554
  lines=5,
555
  elem_classes=["url-box"]
556
  )
557
 
558
- # 可折叠的预览区域
559
- with gr.Accordion("🖼️ 图片预览", open=False) as preview_accordion:
560
  upload_preview = gr.Gallery(
561
  label="上传预览",
562
  show_label=False,
@@ -575,38 +552,28 @@ with gr.Blocks(title="私人图床服务", theme=gr.themes.Soft(), css=custom_cs
575
 
576
  upload_btn.click(
577
  lambda files, desc, pwd: upload_images([f.name for f in files] if files else [], desc, pwd),
578
- inputs=[upload_images_input, upload_desc_input, upload_password_input],
579
  outputs=[upload_output, gr.HTML(visible=False), upload_url_output]
580
  )
581
 
582
  # 图片列表标签页
583
- with gr.Tab("📋 图片列表"):
584
- gr.Markdown("### 查看和管理所有图片")
585
-
586
- with gr.Row():
587
- list_password_input = gr.Textbox(
588
- label="🔐 密码",
589
- type="password",
590
- placeholder="输入访问密码",
591
- scale=3
592
- )
593
- list_refresh_btn = gr.Button("🔄 刷新列表", variant="primary", scale=1)
594
-
595
  list_output = gr.HTML(label="图片列表")
596
 
597
  gr.Markdown("---")
598
- gr.Markdown("### 📷 查看图片详情")
599
 
600
  with gr.Row():
601
  with gr.Column():
602
- view_id_input = gr.Textbox(
603
- label="图片ID",
604
- placeholder="输入图片ID查看详情"
605
  )
606
- view_btn = gr.Button("👁️ 查看详情", variant="secondary")
607
  view_info_output = gr.Markdown(label="图片信息")
608
  view_url_output = gr.Textbox(
609
- label="图片URL(点击复制)",
610
  lines=2,
611
  elem_classes=["url-box"]
612
  )
@@ -614,96 +581,49 @@ with gr.Blocks(title="私人图床服务", theme=gr.themes.Soft(), css=custom_cs
614
  with gr.Column():
615
  view_image_output = gr.Image(label="图片预览", height=400)
616
 
 
 
 
 
 
 
 
 
 
 
 
617
  # 绑定事件
618
  list_refresh_btn.click(
619
  get_image_list_html,
620
- inputs=[list_password_input],
621
  outputs=[list_output]
622
  )
623
 
624
  view_btn.click(
625
- view_image_by_id,
626
- inputs=[view_id_input, list_password_input],
627
  outputs=[view_info_output, view_image_output, view_url_output]
628
  )
629
-
630
- # 删除图片标签页
631
- with gr.Tab("🗑️ 删除图片"):
632
- gr.Markdown("### ⚠️ 删除操作不可恢复,请谨慎操作")
633
-
634
- with gr.Row():
635
- with gr.Column():
636
- delete_id_input = gr.Textbox(
637
- label="图片ID",
638
- placeholder="输入要删除的图片ID"
639
- )
640
- delete_password_input = gr.Textbox(
641
- label="🔐 密码",
642
- type="password",
643
- placeholder="输入访问密码"
644
- )
645
- delete_btn = gr.Button("🗑️ 确认删除", variant="stop")
646
- delete_output = gr.Textbox(label="删除结果", lines=3)
647
-
648
- with gr.Column():
649
- gr.Markdown("### 💡 提示")
650
- gr.Markdown("""
651
- - 删除操作会同时删除数据库记录和文件
652
- - 删除后无法恢复
653
- - 可以在图片列表中查看要删除的图片ID
654
- """)
655
-
656
- delete_list_output = gr.HTML(label="剩余图片列表")
657
 
658
  delete_btn.click(
659
- delete_image,
660
- inputs=[delete_id_input, delete_password_input],
661
- outputs=[delete_output, delete_list_output]
662
  )
663
 
664
- # 统计信息标签页
665
- with gr.Tab("📊 统计信息"):
666
- stats_password_input = gr.Textbox(
667
- label="🔐 密码",
668
- type="password",
669
- placeholder="输入访问密码"
670
- )
671
- stats_btn = gr.Button("📊 查看统计", variant="primary")
672
- stats_output = gr.Markdown(label="统计信息")
673
-
674
- gr.Markdown("---")
675
- gr.Markdown("### 📦 导出数据")
676
  gr.Markdown("导出所有图片的元数据(包含完整URL)为JSON格式")
677
 
678
- export_btn = gr.Button("📦 导出元数据(JSON)", variant="secondary")
679
  export_output = gr.Textbox(label="导出结果", lines=2)
680
  export_file_output = gr.File(label="下载文件")
681
 
682
- stats_btn.click(
683
- get_stats,
684
- inputs=[stats_password_input],
685
- outputs=[stats_output]
686
- )
687
-
688
  export_btn.click(
689
  export_data,
690
- inputs=[stats_password_input],
691
  outputs=[export_output, export_file_output]
692
  )
693
-
694
- gr.Markdown("---")
695
- gr.Markdown("""
696
- ### 💡 使用提示
697
- - **完整URL**: 在环境变量中配置 `HF_USERNAME` 和 `HF_SPACE_NAME` 即可生成完整的外部访问URL
698
- - **批量上传**: 支持一次选择多张图片上传
699
- - **快速复制**: 在图片列表中点击"复制URL"按钮即可复制到剪贴板
700
- - **数据安全**: 定期导出元数据进行备份
701
-
702
- ### 🔒 安全提示
703
- - 建议将 Space 设为 Private 以保护隐私
704
- - 定期更换访问密码
705
- - 不要分享完整URL给不信任的人
706
- """)
707
 
708
  if __name__ == "__main__":
709
  # 将Gradio app挂载到FastAPI
 
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, Response
14
  from fastapi.responses import FileResponse, JSONResponse
15
 
 
35
  cursor.execute("""
36
  CREATE TABLE IF NOT EXISTS images (
37
  id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ hash TEXT NOT NULL UNIQUE,
39
  filename TEXT NOT NULL,
40
  original_filename TEXT NOT NULL,
41
  file_path TEXT NOT NULL,
 
46
  )
47
  """)
48
 
49
+ # 创建hash索引以提高查询速度
50
+ cursor.execute("""
51
+ CREATE INDEX IF NOT EXISTS idx_hash ON images(hash)
52
+ """)
53
+
54
  conn.commit()
55
  conn.close()
56
+ print("数据库初始化成功")
57
  return True
58
  except Exception as e:
59
+ print(f"数据库初始化失败: {e}")
60
  return False
61
 
62
  def get_db_connection():
 
78
  ext = ".png"
79
  return f"{timestamp}_{hash_suffix}{ext}"
80
 
81
+ def generate_image_hash():
82
+ """生成唯一的图片hash"""
83
+ return uuid.uuid4().hex[:12]
84
+
85
+ def generate_full_url(image_hash):
86
  """生成完整的图片URL(使用专用API端点)"""
87
+ api_path = f"/api/img/{image_hash}"
88
  if HF_USERNAME and HF_SPACE_NAME:
89
  return f"https://{HF_USERNAME}-{HF_SPACE_NAME}.hf.space{api_path}"
90
  else:
91
  # 返回相对路径,用户访问时会自动补全域名
92
  return api_path
93
 
94
+ def keep_alive():
95
+ """防止系统休眠的后台线程"""
96
+ while True:
97
+ # 随机等待1-2分钟
98
+ sleep_time = random.randint(60, 120)
99
+ time.sleep(sleep_time)
100
+ print(f"[Keep-Alive] {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
101
+
102
  def upload_images(images, description, password):
103
  """批量上传图片"""
104
  if not check_password(password):
105
+ return "密码错误", None, ""
106
 
107
  if not images or len(images) == 0:
108
+ return "请选择要上传的图片", None, ""
109
 
110
  results = []
111
  success_count = 0
 
124
  original_filename = f"uploaded_image_{idx+1}.png"
125
  img = image
126
 
127
+ # 生成新文件名和hash
128
  new_filename = generate_filename(original_filename)
129
+ image_hash = generate_image_hash()
130
  file_path = IMAGE_DIR / new_filename
131
 
132
  # 保存图片
 
142
  cursor = conn.cursor()
143
  cursor.execute(
144
  """
145
+ INSERT INTO images (hash, filename, original_filename, file_path, file_size, mime_type, upload_time, description)
146
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
147
  """,
148
+ (image_hash, new_filename, original_filename, str(file_path), file_size, mime_type, upload_time, description or "")
149
  )
150
  image_id = cursor.lastrowid
151
  conn.commit()
152
  conn.close()
153
 
154
  # 生成访问 URL
155
+ full_url = generate_full_url(image_hash)
156
 
157
  results.append({
158
+ 'hash': image_hash,
159
  'filename': original_filename,
160
  'size': file_size,
161
  'url': full_url,
 
172
  fail_count += 1
173
 
174
  # 生成结果文本
175
+ result_text = f"## 上传结果\n\n"
176
+ result_text += f"成功: {success_count} 张 | 失败: {fail_count} 张\n\n"
177
 
178
  for r in results:
179
  if r['status'] == 'success':
180
+ result_text += f"### {r['filename']}\n"
181
+ result_text += f"- **Hash**: {r['hash']}\n"
182
  result_text += f"- **大小**: {r['size'] / 1024:.2f} KB\n"
183
  result_text += f"- **URL**: `{r['url']}`\n\n"
184
  else:
185
+ result_text += f"### {r['filename']}\n"
186
  result_text += f"- **错误**: {r['error']}\n\n"
187
 
188
  # 生成URL列表(用于复制)
 
191
  return result_text, get_image_list_html(password), url_list
192
 
193
  except Exception as e:
194
+ return f"上传失败: {str(e)}", None, ""
195
 
196
  def get_image_list_html(password):
197
  """获取图片列表(HTML格式)"""
198
  if not check_password(password):
199
+ return "<p style='color: red;'>密码错误</p>"
200
 
201
  try:
202
  conn = get_db_connection()
203
  cursor = conn.cursor()
204
  cursor.execute(
205
  """
206
+ SELECT id, hash, filename, original_filename, file_path, file_size, upload_time, description
207
  FROM images
208
  ORDER BY upload_time DESC
209
  """
 
221
  .image-table th, .image-table td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
222
  .image-table th { background-color: #f0f0f0; font-weight: bold; }
223
  .image-table tr:hover { background-color: #f5f5f5; }
 
224
  .btn-copy { background: #2196F3; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; margin-right: 5px; }
225
  .btn-delete { background: #f44336; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; }
226
+ .image-hash { font-family: monospace; color: #666; font-size: 12px; }
227
  </style>
228
  <table class="image-table">
229
  <thead>
230
  <tr>
231
+ <th>Hash</th>
232
  <th>文件名</th>
233
  <th>大小</th>
234
  <th>上传时间</th>
 
241
 
242
  for row in rows:
243
  upload_time_str = datetime.fromisoformat(row['upload_time']).strftime('%Y-%m-%d %H:%M:%S')
244
+ full_url = generate_full_url(row['hash'])
245
 
246
  html += f"""
247
+ <tr id="row-{row['hash']}">
248
+ <td class="image-hash">{row['hash']}</td>
249
  <td>{row['original_filename']}</td>
250
  <td>{row['file_size'] / 1024:.2f} KB</td>
251
  <td>{upload_time_str}</td>
252
  <td>{row['description'] or '-'}</td>
253
  <td>
254
+ <button class="btn-copy" onclick="navigator.clipboard.writeText('{full_url}').then(() => alert('URL已复制'))">复制URL</button>
255
+ <button class="btn-delete" data-hash="{row['hash']}" data-filename="{row['original_filename']}">删除</button>
256
  </td>
257
  </tr>
258
  """
 
265
  return html
266
 
267
  except Exception as e:
268
+ return f"<p style='color: red;'>获取列表失败: {str(e)}</p>"
269
 
270
+ def view_image_by_hash(image_hash, password):
271
+ """通过hash查看图片"""
272
  if not check_password(password):
273
+ return "密码错误", None, ""
274
 
275
+ if not image_hash:
276
+ return "请输入图片Hash", None, ""
277
 
278
  try:
279
  conn = get_db_connection()
280
  cursor = conn.cursor()
281
  cursor.execute(
282
+ "SELECT * FROM images WHERE hash = ?",
283
+ (image_hash,)
284
  )
285
  row = cursor.fetchone()
286
  conn.close()
287
 
288
  if not row:
289
+ return "图片不存在", None, ""
290
 
291
  file_path = Path(row['file_path'])
292
 
293
  if not file_path.exists():
294
+ return "图片文件已损坏或丢失", None, ""
295
 
296
  # 生成访问 URL
297
+ full_url = generate_full_url(row['hash'])
298
 
299
+ info_text = f"""## 图片信息
300
 
301
+ **Hash**: {row['hash']}
302
  **原始文件名**: {row['original_filename']}
303
  **存储文件名**: {row['filename']}
304
  **大小**: {row['file_size'] / 1024:.2f} KB
 
314
  return info_text, str(file_path), full_url
315
 
316
  except Exception as e:
317
+ return f"查看失败: {str(e)}", None, ""
318
 
319
+ def delete_image_by_hash(image_hash, password):
320
+ """通过hash删除图片"""
321
  if not check_password(password):
322
+ return "密码错误", None
323
 
324
+ if not image_hash:
325
+ return "请输入图片Hash", None
326
 
327
  try:
328
  conn = get_db_connection()
329
  cursor = conn.cursor()
330
 
331
  # 获取图片信息
332
+ cursor.execute("SELECT * FROM images WHERE hash = ?", (image_hash,))
333
  row = cursor.fetchone()
334
 
335
  if not row:
336
  conn.close()
337
+ return "图片不存在", None
338
 
339
  file_path = Path(row['file_path'])
340
  original_filename = row['original_filename']
341
 
342
  # 删除数据库记录
343
+ cursor.execute("DELETE FROM images WHERE hash = ?", (image_hash,))
344
  conn.commit()
345
  conn.close()
346
 
 
348
  if file_path.exists():
349
  file_path.unlink()
350
 
351
+ return f"已删除图片: {original_filename}", get_image_list_html(password)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
 
353
  except Exception as e:
354
+ return f"删除失败: {str(e)}", None
355
 
356
  def export_data(password):
357
  """导出数据(仅元数据)"""
358
  if not check_password(password):
359
+ return "密码错误", None
360
 
361
  try:
362
  conn = get_db_connection()
 
368
  # 转换为 JSON
369
  data = []
370
  for row in rows:
371
+ full_url = generate_full_url(row['hash'])
372
  data.append({
373
+ 'hash': row['hash'],
374
  'filename': row['filename'],
375
  'original_filename': row['original_filename'],
376
  'file_size': row['file_size'],
 
384
  with open(export_path, 'w', encoding='utf-8') as f:
385
  json.dump(data, f, ensure_ascii=False, indent=2)
386
 
387
+ return f"元数据已导出到: {export_path}", export_path
388
 
389
  except Exception as e:
390
+ return f"导出失败: {str(e)}", None
391
 
392
  # 初始化数据库
393
  init_success = init_db()
394
 
395
+ # 启动防休眠线程
396
+ keep_alive_thread = threading.Thread(target=keep_alive, daemon=True)
397
+ keep_alive_thread.start()
398
+
399
  # 创建FastAPI app
400
  fastapi_app = FastAPI()
401
 
402
  # 添加图片访问API端点
403
+ @fastapi_app.get("/api/img/{image_hash}")
404
+ async def get_image(image_hash: str):
405
+ """通过hash获取图片"""
406
  try:
407
  conn = get_db_connection()
408
  cursor = conn.cursor()
409
+ cursor.execute("SELECT * FROM images WHERE hash = ?", (image_hash,))
410
  row = cursor.fetchone()
411
  conn.close()
412
 
 
441
  )
442
 
443
  # 添加图片信息API端点(可选,用于获取元数据)
444
+ @fastapi_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(
 
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
 
 
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:
 
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",
 
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,
 
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
  )
 
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
  if __name__ == "__main__":
629
  # 将Gradio app挂载到FastAPI