Norcoo commited on
Commit
8f06566
·
verified ·
1 Parent(s): 8024afe

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +353 -435
  2. requirements.txt +2 -1
app.py CHANGED
@@ -1,11 +1,10 @@
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
@@ -17,22 +16,22 @@ 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
- PUBLIC_DIR = Path("public") # 公共访问目录,使用hash作为文件名
23
  DB_PATH = "image_database.db"
24
 
25
- # 创建图片存储目录
26
  IMAGE_DIR.mkdir(exist_ok=True)
27
- PUBLIC_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,
@@ -46,12 +45,9 @@ def init_db():
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("数据库初始化成功")
@@ -61,17 +57,17 @@ def init_db():
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()
@@ -79,324 +75,399 @@ def generate_filename(original_filename):
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(直接访问public目录)"""
88
- # 查找对应的文件扩展名
89
- try:
90
- conn = get_db_connection()
91
- cursor = conn.cursor()
92
- cursor.execute("SELECT file_path FROM images WHERE hash = ?", (image_hash,))
93
- row = cursor.fetchone()
94
- conn.close()
95
-
96
- if row:
97
- ext = Path(row['file_path']).suffix
98
- public_path = PUBLIC_DIR / f"{image_hash}{ext}"
99
- file_path = f"/file={public_path}"
100
-
101
- if HF_USERNAME and HF_SPACE_NAME:
102
- return f"https://{HF_USERNAME}-{HF_SPACE_NAME}.hf.space{file_path}"
103
- else:
104
- return file_path
105
- except:
106
- pass
107
-
108
- # 默认返回
109
- return f"/file=public/{image_hash}"
110
 
111
  def keep_alive():
112
- """防止系统休眠的后台线程"""
113
  while True:
114
- # 随机等待1-2分钟
115
  sleep_time = random.randint(60, 120)
116
  time.sleep(sleep_time)
117
  print(f"[Keep-Alive] {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
118
 
119
- def upload_images(images, description, password):
120
- """批量上传图片"""
121
- if not check_password(password):
122
- return "密码错误", None, ""
123
-
124
- if not images or len(images) == 0:
125
- return "请选择要上传的图片", None, ""
126
-
127
- results = []
128
- success_count = 0
129
- fail_count = 0
130
-
131
- try:
132
- for idx, image in enumerate(images):
133
- try:
134
- # 获取原始文件名
135
- if isinstance(image, str):
136
- # 如果是文件路径
137
- original_filename = Path(image).name
138
- img = Image.open(image)
139
- else:
140
- # 如果是 PIL Image
141
- original_filename = f"uploaded_image_{idx+1}.png"
142
- img = image
143
-
144
- # 生成新文件名和hash
145
- new_filename = generate_filename(original_filename)
146
- image_hash = generate_image_hash()
147
- file_path = IMAGE_DIR / new_filename
148
-
149
- # 保存图片
150
- img.save(file_path, quality=95, optimize=True)
151
-
152
- # 在public目录创建副本,文件名为hash
153
- ext = file_path.suffix
154
- public_file = PUBLIC_DIR / f"{image_hash}{ext}"
155
- shutil.copy2(file_path, public_file)
156
-
157
- # 获取文件信息
158
- file_size = file_path.stat().st_size
159
- mime_type = f"image/{file_path.suffix[1:]}"
160
- upload_time = datetime.now().isoformat()
161
-
162
- # 插入数据库
163
- conn = get_db_connection()
164
- cursor = conn.cursor()
165
- cursor.execute(
166
- """
167
- INSERT INTO images (hash, filename, original_filename, file_path, file_size, mime_type, upload_time, description)
168
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
169
- """,
170
- (image_hash, new_filename, original_filename, str(file_path), file_size, mime_type, upload_time, description or "")
171
- )
172
- image_id = cursor.lastrowid
173
- conn.commit()
174
- conn.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
- # 生成访问 URL
177
- full_url = generate_full_url(image_hash)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
 
179
- results.append({
180
- 'hash': image_hash,
181
- 'filename': original_filename,
182
- 'size': file_size,
183
- 'url': full_url,
184
- 'status': 'success'
185
- })
186
- success_count += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
187
 
188
- except Exception as e:
189
- results.append({
190
- 'filename': f"图片 {idx+1}",
191
- 'error': str(e),
192
- 'status': 'failed'
193
- })
194
- fail_count += 1
195
-
196
- # 生成结果文本
197
- result_text = f"## 上传结果\n\n"
198
- result_text += f"成功: {success_count} 张 | 失败: {fail_count} 张\n\n"
199
-
200
- for r in results:
201
- if r['status'] == 'success':
202
- result_text += f"### {r['filename']}\n"
203
- result_text += f"- **Hash**: {r['hash']}\n"
204
- result_text += f"- **大小**: {r['size'] / 1024:.2f} KB\n"
205
- result_text += f"- **URL**: `{r['url']}`\n\n"
206
- else:
207
- result_text += f"### {r['filename']}\n"
208
- result_text += f"- **错误**: {r['error']}\n\n"
209
-
210
- # 生成URL列表(用于复制)
211
- url_list = "\n".join([r['url'] for r in results if r['status'] == 'success'])
212
-
213
- return result_text, get_image_list_html(password), url_list
214
-
215
- except Exception as e:
216
- return f"上传失败: {str(e)}", None, ""
217
 
218
- def get_image_list_html(password):
219
- """获取图片列表(HTML格式)"""
220
- if not check_password(password):
221
- return "<p style='color: red;'>密码错误</p>"
222
-
 
223
  try:
224
- conn = get_db_connection()
225
- cursor = conn.cursor()
226
- cursor.execute(
227
- """
228
- SELECT id, hash, filename, original_filename, file_path, file_size, upload_time, description
229
- FROM images
230
- ORDER BY upload_time DESC
231
- """
232
- )
233
- rows = cursor.fetchall()
234
- conn.close()
235
 
236
- if not rows:
237
- return "<p>暂无图片</p>"
238
 
239
- # 生成HTML表格
240
- html = """
241
- <style>
242
- .image-table { width: 100%; border-collapse: collapse; margin-top: 10px; }
243
- .image-table th, .image-table td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
244
- .image-table th { background-color: #f0f0f0; font-weight: bold; }
245
- .image-table tr:hover { background-color: #f5f5f5; }
246
- .btn-copy { background: #2196F3; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; margin-right: 5px; }
247
- .btn-delete { background: #f44336; color: white; padding: 6px 12px; border: none; border-radius: 4px; cursor: pointer; }
248
- .image-hash { font-family: monospace; color: #666; font-size: 12px; }
249
- </style>
250
- <table class="image-table">
251
- <thead>
252
- <tr>
253
- <th>Hash</th>
254
- <th>文件名</th>
255
- <th>大小</th>
256
- <th>上传时间</th>
257
- <th>描述</th>
258
- <th>操作</th>
259
- </tr>
260
- </thead>
261
- <tbody>
262
- """
263
 
264
- for row in rows:
265
- upload_time_str = datetime.fromisoformat(row['upload_time']).strftime('%Y-%m-%d %H:%M:%S')
266
- full_url = generate_full_url(row['hash'])
 
267
 
268
- html += f"""
269
- <tr id="row-{row['hash']}">
270
- <td class="image-hash">{row['hash']}</td>
271
- <td>{row['original_filename']}</td>
272
- <td>{row['file_size'] / 1024:.2f} KB</td>
273
- <td>{upload_time_str}</td>
274
- <td>{row['description'] or '-'}</td>
275
- <td>
276
- <button class="btn-copy" onclick="navigator.clipboard.writeText('{full_url}').then(() => alert('URL已复制'))">复制URL</button>
277
- <button class="btn-delete" data-hash="{row['hash']}" data-filename="{row['original_filename']}">删除</button>
278
- </td>
279
- </tr>
280
- """
281
-
282
- html += """
283
- </tbody>
284
- </table>
285
- """
 
 
 
 
 
 
 
 
 
 
 
286
 
287
- return html
288
-
289
  except Exception as e:
290
- return f"<p style='color: red;'>获取列表失败: {str(e)}</p>"
291
-
292
- def view_image_by_hash(image_hash, password):
293
- """通过hash查看图片"""
294
- if not check_password(password):
295
- return "密码错误", None, ""
296
-
297
- if not image_hash:
298
- return "请输入图片Hash", None, ""
299
-
300
  try:
301
  conn = get_db_connection()
302
  cursor = conn.cursor()
303
- cursor.execute(
304
- "SELECT * FROM images WHERE hash = ?",
305
- (image_hash,)
306
- )
307
  row = cursor.fetchone()
308
  conn.close()
309
 
310
  if not row:
311
- return "图片不存在", None, ""
312
 
313
  file_path = Path(row['file_path'])
314
-
315
  if not file_path.exists():
316
- return "图片文件已损坏或丢失", None, ""
317
 
318
- # 生成访问 URL
319
- full_url = generate_full_url(row['hash'])
 
 
 
 
 
 
 
 
320
 
321
- info_text = f"""## 图片信息
322
-
323
- **Hash**: {row['hash']}
324
- **原始文件名**: {row['original_filename']}
325
- **存储文件名**: {row['filename']}
326
- **大小**: {row['file_size'] / 1024:.2f} KB
327
- **上传时间**: {row['upload_time']}
328
- **描述**: {row['description'] or '无'}
329
-
330
- **访问URL**:
331
- ```
332
- {full_url}
333
- ```
334
- """
335
 
336
- return info_text, str(file_path), full_url
337
-
 
 
 
 
 
 
 
 
 
 
338
  except Exception as e:
339
- return f"查看失败: {str(e)}", None, ""
340
-
341
- def delete_image_by_hash(image_hash, password):
342
- """通过hash删除图片"""
343
- if not check_password(password):
344
- return "密码错误", None
345
-
346
- if not image_hash:
347
- return "请输入图片Hash", None
348
-
349
  try:
 
 
 
 
 
350
  conn = get_db_connection()
351
  cursor = conn.cursor()
352
-
353
- # 获取图片信息
354
- cursor.execute("SELECT * FROM images WHERE hash = ?", (image_hash,))
355
  row = cursor.fetchone()
356
 
357
  if not row:
358
  conn.close()
359
- return "图片不存在", None
360
 
361
  file_path = Path(row['file_path'])
362
- original_filename = row['original_filename']
363
-
364
- # 删除public目录的副本
365
- ext = file_path.suffix
366
- public_file = PUBLIC_DIR / f"{image_hash}{ext}"
367
- if public_file.exists():
368
- public_file.unlink()
369
-
370
- # 删除数据库记录
371
  cursor.execute("DELETE FROM images WHERE hash = ?", (image_hash,))
372
  conn.commit()
373
  conn.close()
374
 
375
- # 删除原始文件
376
  if file_path.exists():
377
  file_path.unlink()
378
 
379
- return f"已删除图片: {original_filename}", get_image_list_html(password)
380
-
381
  except Exception as e:
382
- return f"删除失败: {str(e)}", None
383
 
384
- def export_data(password):
385
- """导出数据(仅元数据)"""
386
- if not check_password(password):
387
- return "密码错误", None
388
-
389
  try:
 
 
 
 
390
  conn = get_db_connection()
391
  cursor = conn.cursor()
392
  cursor.execute("SELECT * FROM images ORDER BY upload_time DESC")
393
  rows = cursor.fetchall()
394
  conn.close()
395
 
396
- # 转换为 JSON
397
  data = []
398
  for row in rows:
399
- full_url = generate_full_url(row['hash'])
400
  data.append({
401
  'hash': row['hash'],
402
  'filename': row['filename'],
@@ -404,171 +475,18 @@ def export_data(password):
404
  'file_size': row['file_size'],
405
  'upload_time': row['upload_time'],
406
  'description': row['description'],
407
- 'url': full_url
408
  })
409
 
410
- # 保存为文件
411
- export_path = "image_metadata_export.json"
412
- with open(export_path, 'w', encoding='utf-8') as f:
413
- json.dump(data, f, ensure_ascii=False, indent=2)
414
-
415
- return f"元数据已导出到: {export_path}", export_path
416
-
417
  except Exception as e:
418
- return f"导出失败: {str(e)}", None
419
 
420
- # 初始化数据库
421
- init_success = init_db()
422
-
423
- # 启动防休眠线程
424
- keep_alive_thread = threading.Thread(target=keep_alive, daemon=True)
425
- keep_alive_thread.start()
426
-
427
- # 自定义CSS
428
- custom_css = """
429
- .compact-gallery .gallery {
430
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)) !important;
431
- }
432
- .url-box textarea {
433
- font-family: monospace;
434
- font-size: 12px;
435
- }
436
- """
437
-
438
- # 创建 Gradio 界面
439
- with gr.Blocks(title="My图床", theme=gr.themes.Soft(), css=custom_css) as gradio_app:
440
- gr.Markdown("# My图床服务")
441
-
442
- # 全局密码输入
443
- with gr.Row():
444
- global_password = gr.Textbox(
445
- label="访问密码",
446
- type="password",
447
- placeholder="输入密码以使用所有功能",
448
- scale=4
449
- )
450
-
451
- if not init_success:
452
- gr.Markdown("## 数据库初始化失败")
453
- else:
454
- with gr.Tabs():
455
- # 上传图片标签页
456
- with gr.Tab("上传图片"):
457
- with gr.Row():
458
- with gr.Column(scale=1):
459
- upload_images_input = gr.File(
460
- label="选择图片(支持多选)",
461
- file_count="multiple",
462
- file_types=["image"]
463
- )
464
- upload_desc_input = gr.Textbox(
465
- label="描述(可选)",
466
- placeholder="为这批图片添加描述...",
467
- lines=2
468
- )
469
- upload_btn = gr.Button("上传", variant="primary")
470
-
471
- with gr.Column(scale=1):
472
- upload_output = gr.Markdown(label="上传结果")
473
- upload_url_output = gr.Textbox(
474
- label="图片URL列表(可复制)",
475
- lines=5,
476
- elem_classes=["url-box"]
477
- )
478
-
479
- # 预览区域
480
- with gr.Accordion("图片预览", open=False):
481
- upload_preview = gr.Gallery(
482
- label="上传预览",
483
- show_label=False,
484
- columns=4,
485
- rows=2,
486
- height="auto",
487
- elem_classes=["compact-gallery"]
488
- )
489
-
490
- # 绑定事件
491
- upload_images_input.change(
492
- lambda files: [f.name for f in files] if files else [],
493
- inputs=[upload_images_input],
494
- outputs=[upload_preview]
495
- )
496
-
497
- upload_btn.click(
498
- lambda files, desc, pwd: upload_images([f.name for f in files] if files else [], desc, pwd),
499
- inputs=[upload_images_input, upload_desc_input, global_password],
500
- outputs=[upload_output, gr.HTML(visible=False), upload_url_output]
501
- )
502
-
503
- # 图片列表标签页
504
- with gr.Tab("图片列表"):
505
- list_refresh_btn = gr.Button("刷新列表", variant="primary")
506
- list_output = gr.HTML(label="图片列表")
507
-
508
- gr.Markdown("---")
509
- gr.Markdown("### 查看图片详情")
510
-
511
- with gr.Row():
512
- with gr.Column():
513
- view_hash_input = gr.Textbox(
514
- label="图片Hash",
515
- placeholder="输入图片Hash查看详情"
516
- )
517
- view_btn = gr.Button("查看详情", variant="secondary")
518
- view_info_output = gr.Markdown(label="图片信息")
519
- view_url_output = gr.Textbox(
520
- label="图片URL",
521
- lines=2,
522
- elem_classes=["url-box"]
523
- )
524
-
525
- with gr.Column():
526
- view_image_output = gr.Image(label="图片预览", height=400)
527
-
528
- # 删除图片区域
529
- gr.Markdown("---")
530
- gr.Markdown("### 删除图片")
531
- with gr.Row():
532
- delete_hash_input = gr.Textbox(
533
- label="图片Hash",
534
- placeholder="输入要删除的图片Hash"
535
- )
536
- delete_btn = gr.Button("删除", variant="stop")
537
- delete_output = gr.Textbox(label="删除结果", lines=2)
538
-
539
- # 绑定事件
540
- list_refresh_btn.click(
541
- get_image_list_html,
542
- inputs=[global_password],
543
- outputs=[list_output]
544
- )
545
-
546
- view_btn.click(
547
- view_image_by_hash,
548
- inputs=[view_hash_input, global_password],
549
- outputs=[view_info_output, view_image_output, view_url_output]
550
- )
551
-
552
- delete_btn.click(
553
- delete_image_by_hash,
554
- inputs=[delete_hash_input, global_password],
555
- outputs=[delete_output, list_output]
556
- )
557
-
558
- # 数据导出标签页
559
- with gr.Tab("导出数据"):
560
- gr.Markdown("导出所有图片的元数据(包含完整URL)为JSON格式")
561
-
562
- export_btn = gr.Button("导出元数据(JSON)", variant="primary")
563
- export_output = gr.Textbox(label="导出结果", lines=2)
564
- export_file_output = gr.File(label="下载文件")
565
-
566
- export_btn.click(
567
- export_data,
568
- inputs=[global_password],
569
- outputs=[export_output, export_file_output]
570
- )
571
-
572
- if __name__ == "__main__":
573
- # 直接启动Gradio,它会使用自己的FastAPI实例
574
- gradio_app.launch(server_name="0.0.0.0", server_port=7860)
 
1
+ from flask import Flask, render_template_string, request, send_file, jsonify, redirect, url_for, session
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 uuid
9
  import threading
10
  import time
 
16
  HF_USERNAME = os.environ.get("HF_USERNAME", "")
17
  HF_SPACE_NAME = os.environ.get("HF_SPACE_NAME", "")
18
 
19
+ # Flask应用
20
+ app = Flask(__name__)
21
+ app.secret_key = os.environ.get("ACCESS_PASSWORD", "your-secret-key-change-this")
22
+
23
  # 文件存储路径
24
  IMAGE_DIR = Path("uploaded_images")
 
25
  DB_PATH = "image_database.db"
26
 
27
+ # 创建目录
28
  IMAGE_DIR.mkdir(exist_ok=True)
 
29
 
30
  def init_db():
31
  """初始化数据库"""
32
  try:
33
  conn = sqlite3.connect(DB_PATH)
34
  cursor = conn.cursor()
 
 
35
  cursor.execute("""
36
  CREATE TABLE IF NOT EXISTS images (
37
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 
45
  description TEXT
46
  )
47
  """)
 
 
48
  cursor.execute("""
49
  CREATE INDEX IF NOT EXISTS idx_hash ON images(hash)
50
  """)
 
51
  conn.commit()
52
  conn.close()
53
  print("数据库初始化成功")
 
57
  return False
58
 
59
  def get_db_connection():
 
60
  conn = sqlite3.connect(DB_PATH)
61
  conn.row_factory = sqlite3.Row
62
  return conn
63
 
64
  def check_password(password):
 
65
  return password == ACCESS_PASSWORD
66
 
67
+ def generate_image_hash():
68
+ return uuid.uuid4().hex[:12]
69
+
70
  def generate_filename(original_filename):
 
71
  timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f')
72
  hash_suffix = hashlib.md5(str(datetime.now().timestamp()).encode()).hexdigest()[:8]
73
  ext = Path(original_filename).suffix.lower()
 
75
  ext = ".png"
76
  return f"{timestamp}_{hash_suffix}{ext}"
77
 
 
 
 
 
78
  def generate_full_url(image_hash):
79
+ """生成完整的图片URL"""
80
+ if HF_USERNAME and HF_SPACE_NAME:
81
+ return f"https://{HF_USERNAME}-{HF_SPACE_NAME}.hf.space/img/{image_hash}"
82
+ else:
83
+ return f"/img/{image_hash}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
  def keep_alive():
86
+ """防止系统休眠"""
87
  while True:
 
88
  sleep_time = random.randint(60, 120)
89
  time.sleep(sleep_time)
90
  print(f"[Keep-Alive] {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
91
 
92
+ # 初始化
93
+ init_db()
94
+ keep_alive_thread = threading.Thread(target=keep_alive, daemon=True)
95
+ keep_alive_thread.start()
96
+
97
+ # HTML模板
98
+ HTML_TEMPLATE = """
99
+ <!DOCTYPE html>
100
+ <html lang="zh-CN">
101
+ <head>
102
+ <meta charset="UTF-8">
103
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
104
+ <title>My图床服务</title>
105
+ <style>
106
+ * { margin: 0; padding: 0; box-sizing: border-box; }
107
+ body { font-family: Arial, sans-serif; padding: 20px; background: #f5f5f5; }
108
+ .container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
109
+ h1 { margin-bottom: 10px; }
110
+ .auth-box { margin: 20px 0; padding: 15px; background: #f9f9f9; border-radius: 5px; }
111
+ .auth-box input { padding: 10px; width: 300px; border: 1px solid #ddd; border-radius: 4px; }
112
+ .tabs { display: flex; gap: 10px; margin: 20px 0; border-bottom: 2px solid #ddd; }
113
+ .tab { padding: 10px 20px; cursor: pointer; border: none; background: none; font-size: 16px; }
114
+ .tab.active { border-bottom: 3px solid #007bff; color: #007bff; }
115
+ .tab-content { display: none; padding: 20px 0; }
116
+ .tab-content.active { display: block; }
117
+ .upload-area { border: 2px dashed #ddd; padding: 40px; text-align: center; border-radius: 8px; margin: 20px 0; }
118
+ .upload-area:hover { border-color: #007bff; background: #f9f9f9; }
119
+ input[type="file"] { display: none; }
120
+ .btn { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
121
+ .btn:hover { background: #0056b3; }
122
+ .btn-danger { background: #dc3545; }
123
+ .btn-danger:hover { background: #c82333; }
124
+ textarea, input[type="text"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; margin: 10px 0; }
125
+ .result { margin: 20px 0; padding: 15px; background: #e7f3ff; border-radius: 5px; white-space: pre-wrap; word-break: break-all; }
126
+ table { width: 100%; border-collapse: collapse; margin: 20px 0; }
127
+ th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
128
+ th { background: #f0f0f0; font-weight: bold; }
129
+ tr:hover { background: #f9f9f9; }
130
+ .hash { font-family: monospace; font-size: 12px; color: #666; }
131
+ .btn-small { padding: 6px 12px; font-size: 12px; margin: 0 2px; }
132
+ </style>
133
+ </head>
134
+ <body>
135
+ <div class="container">
136
+ <h1>My图床服务</h1>
137
+
138
+ <div class="auth-box">
139
+ <label>访问密码:</label>
140
+ <input type="password" id="password" placeholder="输入密码以使用所有功能">
141
+ </div>
142
+
143
+ <div class="tabs">
144
+ <button class="tab active" onclick="showTab('upload')">上传图片</button>
145
+ <button class="tab" onclick="showTab('list')">图片列表</button>
146
+ <button class="tab" onclick="showTab('export')">导出数据</button>
147
+ </div>
148
+
149
+ <div id="upload" class="tab-content active">
150
+ <h2>上传图片</h2>
151
+ <div class="upload-area" onclick="document.getElementById('fileInput').click()">
152
+ <p>点击选择图片(支持多选)</p>
153
+ <input type="file" id="fileInput" multiple accept="image/*">
154
+ </div>
155
+ <div>
156
+ <label>描述(可选):</label>
157
+ <textarea id="description" rows="3" placeholder="为这批图片添加描述..."></textarea>
158
+ </div>
159
+ <button class="btn" onclick="uploadImages()">上传</button>
160
+ <div id="uploadResult" class="result" style="display:none;"></div>
161
+ </div>
162
+
163
+ <div id="list" class="tab-content">
164
+ <h2>图片列表</h2>
165
+ <button class="btn" onclick="loadImages()">刷新列表</button>
166
+ <div id="imageList"></div>
167
+ </div>
168
+
169
+ <div id="export" class="tab-content">
170
+ <h2>导出数据</h2>
171
+ <p>导出所有图片的��数据(包含完整URL)为JSON格式</p>
172
+ <button class="btn" onclick="exportData()">导出元数据</button>
173
+ <div id="exportResult" class="result" style="display:none;"></div>
174
+ </div>
175
+ </div>
176
+
177
+ <script>
178
+ function showTab(tabName) {
179
+ document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
180
+ document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));
181
+ document.getElementById(tabName).classList.add('active');
182
+ event.target.classList.add('active');
183
+ }
184
+
185
+ async function uploadImages() {
186
+ const password = document.getElementById('password').value;
187
+ const files = document.getElementById('fileInput').files;
188
+ const description = document.getElementById('description').value;
189
+
190
+ if (!password) {
191
+ alert('请输入密码');
192
+ return;
193
+ }
194
+ if (files.length === 0) {
195
+ alert('请选择图片');
196
+ return;
197
+ }
198
+
199
+ const formData = new FormData();
200
+ formData.append('password', password);
201
+ formData.append('description', description);
202
+ for (let file of files) {
203
+ formData.append('images', file);
204
+ }
205
+
206
+ try {
207
+ const response = await fetch('/api/upload', {
208
+ method: 'POST',
209
+ body: formData
210
+ });
211
+ const result = await response.json();
212
+ const resultDiv = document.getElementById('uploadResult');
213
+ resultDiv.style.display = 'block';
214
 
215
+ if (result.success) {
216
+ let html = `成功上传 ${result.data.length} 张图片:\\n\\n`;
217
+ result.data.forEach(img => {
218
+ html += `Hash: ${img.hash}\\nURL: ${img.url}\\n\\n`;
219
+ });
220
+ resultDiv.textContent = html;
221
+ } else {
222
+ resultDiv.textContent = '错误: ' + result.error;
223
+ }
224
+ } catch (error) {
225
+ alert('上传失败: ' + error);
226
+ }
227
+ }
228
+
229
+ async function loadImages() {
230
+ const password = document.getElementById('password').value;
231
+ if (!password) {
232
+ alert('请输入密码');
233
+ return;
234
+ }
235
+
236
+ try {
237
+ const response = await fetch(`/api/images?password=${password}`);
238
+ const result = await response.json();
239
+ const listDiv = document.getElementById('imageList');
240
 
241
+ if (result.success) {
242
+ if (result.data.length === 0) {
243
+ listDiv.innerHTML = '<p>暂无图片</p>';
244
+ return;
245
+ }
246
+
247
+ let html = '<table><thead><tr><th>Hash</th><th>文件名</th><th>大小</th><th>上传时间</th><th>操作</th></tr></thead><tbody>';
248
+ result.data.forEach(img => {
249
+ html += `<tr>
250
+ <td class="hash">${img.hash}</td>
251
+ <td>${img.original_filename}</td>
252
+ <td>${(img.file_size / 1024).toFixed(2)} KB</td>
253
+ <td>${img.upload_time}</td>
254
+ <td>
255
+ <button class="btn btn-small" onclick="copyUrl('${img.url}')">复制URL</button>
256
+ <button class="btn btn-small btn-danger" onclick="deleteImage('${img.hash}')">删除</button>
257
+ </td>
258
+ </tr>`;
259
+ });
260
+ html += '</tbody></table>';
261
+ listDiv.innerHTML = html;
262
+ } else {
263
+ listDiv.innerHTML = '<p>错误: ' + result.error + '</p>';
264
+ }
265
+ } catch (error) {
266
+ alert('加载失败: ' + error);
267
+ }
268
+ }
269
+
270
+ function copyUrl(url) {
271
+ navigator.clipboard.writeText(url).then(() => alert('URL已复制到剪贴板'));
272
+ }
273
+
274
+ async function deleteImage(hash) {
275
+ if (!confirm('确定要删除这张图片吗?')) return;
276
+
277
+ const password = document.getElementById('password').value;
278
+ try {
279
+ const response = await fetch(`/api/delete/${hash}`, {
280
+ method: 'POST',
281
+ headers: {'Content-Type': 'application/json'},
282
+ body: JSON.stringify({password})
283
+ });
284
+ const result = await response.json();
285
+ if (result.success) {
286
+ alert('删除成功');
287
+ loadImages();
288
+ } else {
289
+ alert('删除失败: ' + result.error);
290
+ }
291
+ } catch (error) {
292
+ alert('删除失败: ' + error);
293
+ }
294
+ }
295
+
296
+ async function exportData() {
297
+ const password = document.getElementById('password').value;
298
+ if (!password) {
299
+ alert('请输入密码');
300
+ return;
301
+ }
302
+
303
+ try {
304
+ const response = await fetch(`/api/export?password=${password}`);
305
+ const blob = await response.blob();
306
+ const url = window.URL.createObjectURL(blob);
307
+ const a = document.createElement('a');
308
+ a.href = url;
309
+ a.download = 'image_metadata.json';
310
+ a.click();
311
 
312
+ document.getElementById('exportResult').style.display = 'block';
313
+ document.getElementById('exportResult').textContent = '导出成功!';
314
+ } catch (error) {
315
+ alert('导出失败: ' + error);
316
+ }
317
+ }
318
+ </script>
319
+ </body>
320
+ </html>
321
+ """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
 
323
+ @app.route('/')
324
+ def index():
325
+ return render_template_string(HTML_TEMPLATE)
326
+
327
+ @app.route('/api/upload', methods=['POST'])
328
+ def upload():
329
  try:
330
+ password = request.form.get('password')
331
+ if not check_password(password):
332
+ return jsonify({'success': False, 'error': '密码错误'})
 
 
 
 
 
 
 
 
333
 
334
+ files = request.files.getlist('images')
335
+ description = request.form.get('description', '')
336
 
337
+ if not files:
338
+ return jsonify({'success': False, 'error': '没有文件'})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
 
340
+ results = []
341
+ for file in files:
342
+ if file.filename == '':
343
+ continue
344
 
345
+ # 生成hash和文件名
346
+ image_hash = generate_image_hash()
347
+ original_filename = file.filename
348
+ new_filename = generate_filename(original_filename)
349
+ file_path = IMAGE_DIR / new_filename
350
+
351
+ # 保存文件
352
+ file.save(file_path)
353
+
354
+ # 获取信息
355
+ file_size = file_path.stat().st_size
356
+ mime_type = f"image/{file_path.suffix[1:]}"
357
+ upload_time = datetime.now().isoformat()
358
+
359
+ # 保存到数据库
360
+ conn = get_db_connection()
361
+ cursor = conn.cursor()
362
+ cursor.execute(
363
+ "INSERT INTO images (hash, filename, original_filename, file_path, file_size, mime_type, upload_time, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
364
+ (image_hash, new_filename, original_filename, str(file_path), file_size, mime_type, upload_time, description)
365
+ )
366
+ conn.commit()
367
+ conn.close()
368
+
369
+ results.append({
370
+ 'hash': image_hash,
371
+ 'url': generate_full_url(image_hash),
372
+ 'filename': original_filename
373
+ })
374
 
375
+ return jsonify({'success': True, 'data': results})
 
376
  except Exception as e:
377
+ return jsonify({'success': False, 'error': str(e)})
378
+
379
+ @app.route('/img/<image_hash>')
380
+ def serve_image(image_hash):
 
 
 
 
 
 
381
  try:
382
  conn = get_db_connection()
383
  cursor = conn.cursor()
384
+ cursor.execute("SELECT file_path, mime_type, original_filename FROM images WHERE hash = ?", (image_hash,))
 
 
 
385
  row = cursor.fetchone()
386
  conn.close()
387
 
388
  if not row:
389
+ return "Image not found", 404
390
 
391
  file_path = Path(row['file_path'])
 
392
  if not file_path.exists():
393
+ return "File not found", 404
394
 
395
+ return send_file(file_path, mimetype=row['mime_type'])
396
+ except Exception as e:
397
+ return str(e), 500
398
+
399
+ @app.route('/api/images')
400
+ def get_images():
401
+ try:
402
+ password = request.args.get('password')
403
+ if not check_password(password):
404
+ return jsonify({'success': False, 'error': '密码错误'})
405
 
406
+ conn = get_db_connection()
407
+ cursor = conn.cursor()
408
+ cursor.execute("SELECT hash, original_filename, file_size, upload_time, description FROM images ORDER BY upload_time DESC")
409
+ rows = cursor.fetchall()
410
+ conn.close()
 
 
 
 
 
 
 
 
 
411
 
412
+ images = []
413
+ for row in rows:
414
+ images.append({
415
+ 'hash': row['hash'],
416
+ 'original_filename': row['original_filename'],
417
+ 'file_size': row['file_size'],
418
+ 'upload_time': row['upload_time'],
419
+ 'description': row['description'],
420
+ 'url': generate_full_url(row['hash'])
421
+ })
422
+
423
+ return jsonify({'success': True, 'data': images})
424
  except Exception as e:
425
+ return jsonify({'success': False, 'error': str(e)})
426
+
427
+ @app.route('/api/delete/<image_hash>', methods=['POST'])
428
+ def delete_image(image_hash):
 
 
 
 
 
 
429
  try:
430
+ data = request.get_json()
431
+ password = data.get('password')
432
+ if not check_password(password):
433
+ return jsonify({'success': False, 'error': '密码错误'})
434
+
435
  conn = get_db_connection()
436
  cursor = conn.cursor()
437
+ cursor.execute("SELECT file_path FROM images WHERE hash = ?", (image_hash,))
 
 
438
  row = cursor.fetchone()
439
 
440
  if not row:
441
  conn.close()
442
+ return jsonify({'success': False, 'error': '图片不存在'})
443
 
444
  file_path = Path(row['file_path'])
 
 
 
 
 
 
 
 
 
445
  cursor.execute("DELETE FROM images WHERE hash = ?", (image_hash,))
446
  conn.commit()
447
  conn.close()
448
 
 
449
  if file_path.exists():
450
  file_path.unlink()
451
 
452
+ return jsonify({'success': True})
 
453
  except Exception as e:
454
+ return jsonify({'success': False, 'error': str(e)})
455
 
456
+ @app.route('/api/export')
457
+ def export_data():
 
 
 
458
  try:
459
+ password = request.args.get('password')
460
+ if not check_password(password):
461
+ return jsonify({'success': False, 'error': '密码错误'})
462
+
463
  conn = get_db_connection()
464
  cursor = conn.cursor()
465
  cursor.execute("SELECT * FROM images ORDER BY upload_time DESC")
466
  rows = cursor.fetchall()
467
  conn.close()
468
 
 
469
  data = []
470
  for row in rows:
 
471
  data.append({
472
  'hash': row['hash'],
473
  'filename': row['filename'],
 
475
  'file_size': row['file_size'],
476
  'upload_time': row['upload_time'],
477
  'description': row['description'],
478
+ 'url': generate_full_url(row['hash'])
479
  })
480
 
481
+ import json
482
+ from flask import Response
483
+ return Response(
484
+ json.dumps(data, ensure_ascii=False, indent=2),
485
+ mimetype='application/json',
486
+ headers={'Content-Disposition': 'attachment; filename=image_metadata.json'}
487
+ )
488
  except Exception as e:
489
+ return jsonify({'success': False, 'error': str(e)})
490
 
491
+ if __name__ == '__main__':
492
+ app.run(host='0.0.0.0', port=7860, debug=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,3 +1,4 @@
1
- gradio==4.44.0
2
  Pillow==10.4.0
 
3
 
 
1
+ Flask==3.0.0
2
  Pillow==10.4.0
3
+ Werkzeug==3.0.1
4