|
|
from flask import Flask, render_template_string, request, send_file, jsonify, redirect, url_for, session |
|
|
import os |
|
|
import sqlite3 |
|
|
import hashlib |
|
|
from datetime import datetime |
|
|
from pathlib import Path |
|
|
from PIL import Image |
|
|
import uuid |
|
|
import threading |
|
|
import time |
|
|
import random |
|
|
import shutil |
|
|
|
|
|
|
|
|
ACCESS_PASSWORD = os.environ.get("ACCESS_PASSWORD", "changeme") |
|
|
HF_USERNAME = os.environ.get("HF_USERNAME", "") |
|
|
HF_SPACE_NAME = os.environ.get("HF_SPACE_NAME", "") |
|
|
|
|
|
|
|
|
app = Flask(__name__) |
|
|
app.secret_key = os.environ.get("ACCESS_PASSWORD", "your-secret-key-change-this") |
|
|
|
|
|
|
|
|
IMAGE_DIR = Path("uploaded_images") |
|
|
DB_PATH = "image_database.db" |
|
|
|
|
|
|
|
|
IMAGE_DIR.mkdir(exist_ok=True) |
|
|
|
|
|
def init_db(): |
|
|
"""初始化数据库""" |
|
|
try: |
|
|
conn = sqlite3.connect(DB_PATH) |
|
|
cursor = conn.cursor() |
|
|
cursor.execute(""" |
|
|
CREATE TABLE IF NOT EXISTS images ( |
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
|
|
hash TEXT NOT NULL UNIQUE, |
|
|
filename TEXT NOT NULL, |
|
|
original_filename TEXT NOT NULL, |
|
|
file_path TEXT NOT NULL, |
|
|
file_size INTEGER, |
|
|
mime_type TEXT, |
|
|
upload_time TEXT NOT NULL, |
|
|
description TEXT |
|
|
) |
|
|
""") |
|
|
cursor.execute(""" |
|
|
CREATE INDEX IF NOT EXISTS idx_hash ON images(hash) |
|
|
""") |
|
|
conn.commit() |
|
|
conn.close() |
|
|
print("数据库初始化成功") |
|
|
return True |
|
|
except Exception as e: |
|
|
print(f"数据库初始化失败: {e}") |
|
|
return False |
|
|
|
|
|
def get_db_connection(): |
|
|
conn = sqlite3.connect(DB_PATH) |
|
|
conn.row_factory = sqlite3.Row |
|
|
return conn |
|
|
|
|
|
def check_password(password): |
|
|
return password == ACCESS_PASSWORD |
|
|
|
|
|
def generate_image_hash(): |
|
|
return uuid.uuid4().hex[:12] |
|
|
|
|
|
def generate_filename(original_filename): |
|
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S_%f') |
|
|
hash_suffix = hashlib.md5(str(datetime.now().timestamp()).encode()).hexdigest()[:8] |
|
|
ext = Path(original_filename).suffix.lower() |
|
|
if not ext: |
|
|
ext = ".png" |
|
|
return f"{timestamp}_{hash_suffix}{ext}" |
|
|
|
|
|
def generate_full_url(image_hash): |
|
|
"""生成完整的图片URL""" |
|
|
if HF_USERNAME and HF_SPACE_NAME: |
|
|
return f"https://{HF_USERNAME}-{HF_SPACE_NAME}.hf.space/img/{image_hash}" |
|
|
else: |
|
|
return f"/img/{image_hash}" |
|
|
|
|
|
def keep_alive(): |
|
|
"""防止系统休眠""" |
|
|
while True: |
|
|
sleep_time = random.randint(60, 120) |
|
|
time.sleep(sleep_time) |
|
|
print(f"[Keep-Alive] {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") |
|
|
|
|
|
|
|
|
init_db() |
|
|
keep_alive_thread = threading.Thread(target=keep_alive, daemon=True) |
|
|
keep_alive_thread.start() |
|
|
|
|
|
|
|
|
HTML_TEMPLATE = """ |
|
|
<!DOCTYPE html> |
|
|
<html lang="zh-CN"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>My图床</title> |
|
|
<style> |
|
|
* { margin: 0; padding: 0; box-sizing: border-box; } |
|
|
body { font-family: Arial, sans-serif; padding: 20px; background: #f5f5f5; } |
|
|
.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); } |
|
|
h1 { margin-bottom: 10px; } |
|
|
.auth-box { margin: 20px 0; padding: 15px; background: #f9f9f9; border-radius: 5px; } |
|
|
.auth-box input { padding: 10px; width: 300px; border: 1px solid #ddd; border-radius: 4px; } |
|
|
.tabs { display: flex; gap: 10px; margin: 20px 0; border-bottom: 2px solid #ddd; } |
|
|
.tab { padding: 10px 20px; cursor: pointer; border: none; background: none; font-size: 16px; } |
|
|
.tab.active { border-bottom: 3px solid #007bff; color: #007bff; } |
|
|
.tab-content { display: none; padding: 20px 0; } |
|
|
.tab-content.active { display: block; } |
|
|
.upload-area { border: 2px dashed #ddd; padding: 40px; text-align: center; border-radius: 8px; margin: 20px 0; } |
|
|
.upload-area:hover { border-color: #007bff; background: #f9f9f9; } |
|
|
input[type="file"] { display: none; } |
|
|
.btn { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; } |
|
|
.btn:hover { background: #0056b3; } |
|
|
.btn:disabled { background: #6c757d; cursor: not-allowed; opacity: 0.6; } |
|
|
.btn-danger { background: #dc3545; } |
|
|
.btn-danger:hover { background: #c82333; } |
|
|
.btn-success { background: #28a745; } |
|
|
.btn-success:hover { background: #218838; } |
|
|
textarea, input[type="text"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; margin: 10px 0; } |
|
|
.result { margin: 20px 0; padding: 15px; background: #e7f3ff; border-radius: 5px; white-space: pre-wrap; word-break: break-all; } |
|
|
table { width: 100%; border-collapse: collapse; margin: 20px 0; } |
|
|
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; } |
|
|
th { background: #f0f0f0; font-weight: bold; } |
|
|
tr:hover { background: #f9f9f9; } |
|
|
.hash { font-family: monospace; font-size: 12px; color: #666; } |
|
|
.btn-small { padding: 6px 12px; font-size: 12px; margin: 0 2px; } |
|
|
.file-list-item { padding: 8px; border-bottom: 1px solid #ddd; display: flex; align-items: center; } |
|
|
.file-list-item:last-child { border-bottom: none; } |
|
|
.file-icon { color: #007bff; margin-right: 10px; font-size: 18px; } |
|
|
.file-size { color: #666; font-size: 12px; margin-left: 10px; } |
|
|
.modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0,0,0,0.9); } |
|
|
.modal-content { margin: auto; display: block; max-width: 90%; max-height: 90vh; position: relative; top: 50%; transform: translateY(-50%); } |
|
|
.close { position: absolute; top: 15px; right: 35px; color: #f1f1f1; font-size: 40px; font-weight: bold; cursor: pointer; } |
|
|
.close:hover { color: #bbb; } |
|
|
.modal-info { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); color: white; background: rgba(0,0,0,0.7); padding: 10px 20px; border-radius: 5px; } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="container"> |
|
|
<h1>My图床</h1> |
|
|
|
|
|
<div class="auth-box"> |
|
|
<label>访问密码:</label> |
|
|
<input type="password" id="password" placeholder="输入密码以使用所有功能"> |
|
|
</div> |
|
|
|
|
|
<div class="tabs"> |
|
|
<button class="tab active" onclick="showTab('upload')">上传图片</button> |
|
|
<button class="tab" onclick="showTab('list')">图片列表</button> |
|
|
<button class="tab" onclick="showTab('export')">导出数据</button> |
|
|
</div> |
|
|
|
|
|
<div id="upload" class="tab-content active"> |
|
|
<h2>上传图片</h2> |
|
|
<div class="upload-area" onclick="document.getElementById('fileInput').click()"> |
|
|
<p>点击选择图片(支持多选)</p> |
|
|
<input type="file" id="fileInput" multiple accept="image/*" onchange="showSelectedFiles()"> |
|
|
</div> |
|
|
<div id="selectedFiles" style="display:none; margin: 15px 0; padding: 15px; background: #f9f9f9; border-radius: 5px;"> |
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;"> |
|
|
<h3 style="margin: 0;">已选择 <span id="fileCount">0</span> 张图片(总计:<span id="totalSize">0</span> KB)</h3> |
|
|
<button class="btn btn-small" onclick="clearSelection()">清空选择</button> |
|
|
</div> |
|
|
<ul id="fileList" style="list-style: none; padding: 0;"></ul> |
|
|
</div> |
|
|
<div> |
|
|
<label>描述(可选):</label> |
|
|
<textarea id="description" rows="3" placeholder="为这批图片添加描述..."></textarea> |
|
|
</div> |
|
|
<button class="btn" onclick="uploadImages()">上传</button> |
|
|
<div id="uploadResult" class="result" style="display:none;"></div> |
|
|
</div> |
|
|
|
|
|
<div id="list" class="tab-content"> |
|
|
<h2>图片列表</h2> |
|
|
<button class="btn" onclick="loadImages()">刷新列表</button> |
|
|
<div id="imageList"></div> |
|
|
</div> |
|
|
|
|
|
<div id="export" class="tab-content"> |
|
|
<h2>导出数据</h2> |
|
|
<p>导出所有图片的元数据(包含完整URL)为JSON格式</p> |
|
|
<button class="btn" onclick="exportData()">导出元数据</button> |
|
|
<div id="exportResult" class="result" style="display:none;"></div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- 图片预览模态框 --> |
|
|
<div id="imageModal" class="modal" onclick="closeModal()"> |
|
|
<span class="close">×</span> |
|
|
<img id="modalImage" class="modal-content" onclick="event.stopPropagation()"> |
|
|
<div id="modalInfo" class="modal-info"></div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
function showTab(tabName) { |
|
|
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); |
|
|
document.querySelectorAll('.tab').forEach(el => el.classList.remove('active')); |
|
|
document.getElementById(tabName).classList.add('active'); |
|
|
event.target.classList.add('active'); |
|
|
} |
|
|
|
|
|
function showSelectedFiles() { |
|
|
const fileInput = document.getElementById('fileInput'); |
|
|
const files = fileInput.files; |
|
|
const selectedDiv = document.getElementById('selectedFiles'); |
|
|
const fileList = document.getElementById('fileList'); |
|
|
|
|
|
if (files.length === 0) { |
|
|
selectedDiv.style.display = 'none'; |
|
|
return; |
|
|
} |
|
|
|
|
|
let totalSize = 0; |
|
|
fileList.innerHTML = ''; |
|
|
for (let i = 0; i < files.length; i++) { |
|
|
totalSize += files[i].size; |
|
|
const li = document.createElement('li'); |
|
|
li.className = 'file-list-item'; |
|
|
li.innerHTML = ` |
|
|
<span class="file-icon">📷</span> |
|
|
<span style="flex: 1;">${files[i].name}</span> |
|
|
<span class="file-size">(${(files[i].size / 1024).toFixed(2)} KB)</span> |
|
|
`; |
|
|
fileList.appendChild(li); |
|
|
} |
|
|
|
|
|
document.getElementById('fileCount').textContent = files.length; |
|
|
document.getElementById('totalSize').textContent = (totalSize / 1024).toFixed(2); |
|
|
selectedDiv.style.display = 'block'; |
|
|
} |
|
|
|
|
|
function clearSelection() { |
|
|
document.getElementById('fileInput').value = ''; |
|
|
document.getElementById('selectedFiles').style.display = 'none'; |
|
|
document.getElementById('fileList').innerHTML = ''; |
|
|
} |
|
|
|
|
|
function previewImage(url, filename, size) { |
|
|
const modal = document.getElementById('imageModal'); |
|
|
const modalImg = document.getElementById('modalImage'); |
|
|
const modalInfo = document.getElementById('modalInfo'); |
|
|
|
|
|
modal.style.display = 'block'; |
|
|
modalImg.src = url; |
|
|
modalInfo.innerHTML = `${filename} (${(size / 1024).toFixed(2)} KB)`; |
|
|
} |
|
|
|
|
|
function closeModal() { |
|
|
document.getElementById('imageModal').style.display = 'none'; |
|
|
} |
|
|
|
|
|
// 按ESC键关闭模态框 |
|
|
document.addEventListener('keydown', function(event) { |
|
|
if (event.key === 'Escape') { |
|
|
closeModal(); |
|
|
} |
|
|
}); |
|
|
|
|
|
async function uploadImages() { |
|
|
const password = document.getElementById('password').value; |
|
|
const files = document.getElementById('fileInput').files; |
|
|
const description = document.getElementById('description').value; |
|
|
|
|
|
if (!password) { |
|
|
alert('请输入密码'); |
|
|
return; |
|
|
} |
|
|
if (files.length === 0) { |
|
|
alert('请选择图片'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const formData = new FormData(); |
|
|
formData.append('password', password); |
|
|
formData.append('description', description); |
|
|
for (let file of files) { |
|
|
formData.append('images', file); |
|
|
} |
|
|
|
|
|
// 显示上传中提示 |
|
|
const resultDiv = document.getElementById('uploadResult'); |
|
|
resultDiv.style.display = 'block'; |
|
|
resultDiv.style.background = '#fff3cd'; |
|
|
resultDiv.style.color = '#856404'; |
|
|
resultDiv.innerHTML = '<strong>⏳ 正在上传,请勿关闭页面...</strong><br>正在上传 ' + files.length + ' 张图片'; |
|
|
|
|
|
// 禁用上传按钮 |
|
|
const uploadBtn = event.target; |
|
|
uploadBtn.disabled = true; |
|
|
uploadBtn.textContent = '上传中...'; |
|
|
|
|
|
try { |
|
|
const response = await fetch('/api/upload', { |
|
|
method: 'POST', |
|
|
body: formData |
|
|
}); |
|
|
const result = await response.json(); |
|
|
|
|
|
// 恢复按钮状态 |
|
|
uploadBtn.disabled = false; |
|
|
uploadBtn.textContent = '上传'; |
|
|
|
|
|
resultDiv.style.background = '#e7f3ff'; |
|
|
resultDiv.style.color = 'inherit'; |
|
|
|
|
|
if (result.success) { |
|
|
let html = `<strong>✅ 成功上传 ${result.data.length} 张图片</strong>\\n\\n`; |
|
|
result.data.forEach(img => { |
|
|
html += `Hash: ${img.hash}\\nURL: ${img.url}\\n\\n`; |
|
|
}); |
|
|
resultDiv.textContent = html; |
|
|
|
|
|
// 清空文件选择 |
|
|
clearSelection(); |
|
|
} else { |
|
|
resultDiv.style.background = '#f8d7da'; |
|
|
resultDiv.style.color = '#721c24'; |
|
|
resultDiv.textContent = '❌ 错误: ' + result.error; |
|
|
} |
|
|
} catch (error) { |
|
|
// 恢复按钮状态 |
|
|
uploadBtn.disabled = false; |
|
|
uploadBtn.textContent = '上传'; |
|
|
|
|
|
resultDiv.style.background = '#f8d7da'; |
|
|
resultDiv.style.color = '#721c24'; |
|
|
resultDiv.textContent = '❌ 上传失败: ' + error; |
|
|
} |
|
|
} |
|
|
|
|
|
async function loadImages() { |
|
|
const password = document.getElementById('password').value; |
|
|
if (!password) { |
|
|
alert('请输入密码'); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const response = await fetch(`/api/images?password=${password}`); |
|
|
const result = await response.json(); |
|
|
const listDiv = document.getElementById('imageList'); |
|
|
|
|
|
if (result.success) { |
|
|
if (result.data.length === 0) { |
|
|
listDiv.innerHTML = '<p>暂无图片</p>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
let html = '<table><thead><tr><th>Hash</th><th>文件名</th><th>大小</th><th>上传时间</th><th>操作</th></tr></thead><tbody>'; |
|
|
result.data.forEach(img => { |
|
|
html += `<tr> |
|
|
<td class="hash">${img.hash}</td> |
|
|
<td>${img.original_filename}</td> |
|
|
<td>${(img.file_size / 1024).toFixed(2)} KB</td> |
|
|
<td>${img.upload_time}</td> |
|
|
<td> |
|
|
<button class="btn btn-small btn-success" onclick='previewImage("${img.url}", "${img.original_filename}", ${img.file_size})'>预览</button> |
|
|
<button class="btn btn-small" onclick="copyUrl('${img.url}')">复制URL</button> |
|
|
<button class="btn btn-small btn-danger" onclick="deleteImage('${img.hash}')">删除</button> |
|
|
</td> |
|
|
</tr>`; |
|
|
}); |
|
|
html += '</tbody></table>'; |
|
|
listDiv.innerHTML = html; |
|
|
} else { |
|
|
listDiv.innerHTML = '<p>错误: ' + result.error + '</p>'; |
|
|
} |
|
|
} catch (error) { |
|
|
alert('加载失败: ' + error); |
|
|
} |
|
|
} |
|
|
|
|
|
function copyUrl(url) { |
|
|
navigator.clipboard.writeText(url).then(() => alert('URL已复制到剪贴板')); |
|
|
} |
|
|
|
|
|
async function deleteImage(hash) { |
|
|
if (!confirm('确定要删除这张图片吗?')) return; |
|
|
|
|
|
const password = document.getElementById('password').value; |
|
|
try { |
|
|
const response = await fetch(`/api/delete/${hash}`, { |
|
|
method: 'POST', |
|
|
headers: {'Content-Type': 'application/json'}, |
|
|
body: JSON.stringify({password}) |
|
|
}); |
|
|
const result = await response.json(); |
|
|
if (result.success) { |
|
|
alert('删除成功'); |
|
|
loadImages(); |
|
|
} else { |
|
|
alert('删除失败: ' + result.error); |
|
|
} |
|
|
} catch (error) { |
|
|
alert('删除失败: ' + error); |
|
|
} |
|
|
} |
|
|
|
|
|
async function exportData() { |
|
|
const password = document.getElementById('password').value; |
|
|
if (!password) { |
|
|
alert('请输入密码'); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const response = await fetch(`/api/export?password=${password}`); |
|
|
const blob = await response.blob(); |
|
|
const url = window.URL.createObjectURL(blob); |
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = 'image_metadata.json'; |
|
|
a.click(); |
|
|
|
|
|
document.getElementById('exportResult').style.display = 'block'; |
|
|
document.getElementById('exportResult').textContent = '导出成功!'; |
|
|
} catch (error) { |
|
|
alert('导出失败: ' + error); |
|
|
} |
|
|
} |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
""" |
|
|
|
|
|
@app.route('/') |
|
|
def index(): |
|
|
return render_template_string(HTML_TEMPLATE) |
|
|
|
|
|
@app.route('/api/upload', methods=['POST']) |
|
|
def upload(): |
|
|
try: |
|
|
password = request.form.get('password') |
|
|
if not check_password(password): |
|
|
return jsonify({'success': False, 'error': '密码错误'}) |
|
|
|
|
|
files = request.files.getlist('images') |
|
|
description = request.form.get('description', '') |
|
|
|
|
|
if not files: |
|
|
return jsonify({'success': False, 'error': '没有文件'}) |
|
|
|
|
|
results = [] |
|
|
for file in files: |
|
|
if file.filename == '': |
|
|
continue |
|
|
|
|
|
|
|
|
image_hash = generate_image_hash() |
|
|
original_filename = file.filename |
|
|
new_filename = generate_filename(original_filename) |
|
|
file_path = IMAGE_DIR / new_filename |
|
|
|
|
|
|
|
|
file.save(file_path) |
|
|
|
|
|
|
|
|
file_size = file_path.stat().st_size |
|
|
mime_type = f"image/{file_path.suffix[1:]}" |
|
|
upload_time = datetime.now().isoformat() |
|
|
|
|
|
|
|
|
conn = get_db_connection() |
|
|
cursor = conn.cursor() |
|
|
cursor.execute( |
|
|
"INSERT INTO images (hash, filename, original_filename, file_path, file_size, mime_type, upload_time, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", |
|
|
(image_hash, new_filename, original_filename, str(file_path), file_size, mime_type, upload_time, description) |
|
|
) |
|
|
conn.commit() |
|
|
conn.close() |
|
|
|
|
|
results.append({ |
|
|
'hash': image_hash, |
|
|
'url': generate_full_url(image_hash), |
|
|
'filename': original_filename |
|
|
}) |
|
|
|
|
|
return jsonify({'success': True, 'data': results}) |
|
|
except Exception as e: |
|
|
return jsonify({'success': False, 'error': str(e)}) |
|
|
|
|
|
@app.route('/img/<image_hash>') |
|
|
def serve_image(image_hash): |
|
|
try: |
|
|
conn = get_db_connection() |
|
|
cursor = conn.cursor() |
|
|
cursor.execute("SELECT file_path, mime_type, original_filename FROM images WHERE hash = ?", (image_hash,)) |
|
|
row = cursor.fetchone() |
|
|
conn.close() |
|
|
|
|
|
if not row: |
|
|
return "Image not found", 404 |
|
|
|
|
|
file_path = Path(row['file_path']) |
|
|
if not file_path.exists(): |
|
|
return "File not found", 404 |
|
|
|
|
|
return send_file(file_path, mimetype=row['mime_type']) |
|
|
except Exception as e: |
|
|
return str(e), 500 |
|
|
|
|
|
@app.route('/api/images') |
|
|
def get_images(): |
|
|
try: |
|
|
password = request.args.get('password') |
|
|
if not check_password(password): |
|
|
return jsonify({'success': False, 'error': '密码错误'}) |
|
|
|
|
|
conn = get_db_connection() |
|
|
cursor = conn.cursor() |
|
|
cursor.execute("SELECT hash, original_filename, file_size, upload_time, description FROM images ORDER BY upload_time DESC") |
|
|
rows = cursor.fetchall() |
|
|
conn.close() |
|
|
|
|
|
images = [] |
|
|
for row in rows: |
|
|
images.append({ |
|
|
'hash': row['hash'], |
|
|
'original_filename': row['original_filename'], |
|
|
'file_size': row['file_size'], |
|
|
'upload_time': row['upload_time'], |
|
|
'description': row['description'], |
|
|
'url': generate_full_url(row['hash']) |
|
|
}) |
|
|
|
|
|
return jsonify({'success': True, 'data': images}) |
|
|
except Exception as e: |
|
|
return jsonify({'success': False, 'error': str(e)}) |
|
|
|
|
|
@app.route('/api/delete/<image_hash>', methods=['POST']) |
|
|
def delete_image(image_hash): |
|
|
try: |
|
|
data = request.get_json() |
|
|
password = data.get('password') |
|
|
if not check_password(password): |
|
|
return jsonify({'success': False, 'error': '密码错误'}) |
|
|
|
|
|
conn = get_db_connection() |
|
|
cursor = conn.cursor() |
|
|
cursor.execute("SELECT file_path FROM images WHERE hash = ?", (image_hash,)) |
|
|
row = cursor.fetchone() |
|
|
|
|
|
if not row: |
|
|
conn.close() |
|
|
return jsonify({'success': False, 'error': '图片不存在'}) |
|
|
|
|
|
file_path = Path(row['file_path']) |
|
|
cursor.execute("DELETE FROM images WHERE hash = ?", (image_hash,)) |
|
|
conn.commit() |
|
|
conn.close() |
|
|
|
|
|
if file_path.exists(): |
|
|
file_path.unlink() |
|
|
|
|
|
return jsonify({'success': True}) |
|
|
except Exception as e: |
|
|
return jsonify({'success': False, 'error': str(e)}) |
|
|
|
|
|
@app.route('/api/export') |
|
|
def export_data(): |
|
|
try: |
|
|
password = request.args.get('password') |
|
|
if not check_password(password): |
|
|
return jsonify({'success': False, 'error': '密码错误'}) |
|
|
|
|
|
conn = get_db_connection() |
|
|
cursor = conn.cursor() |
|
|
cursor.execute("SELECT * FROM images ORDER BY upload_time DESC") |
|
|
rows = cursor.fetchall() |
|
|
conn.close() |
|
|
|
|
|
data = [] |
|
|
for row in rows: |
|
|
data.append({ |
|
|
'hash': row['hash'], |
|
|
'filename': row['filename'], |
|
|
'original_filename': row['original_filename'], |
|
|
'file_size': row['file_size'], |
|
|
'upload_time': row['upload_time'], |
|
|
'description': row['description'], |
|
|
'url': generate_full_url(row['hash']) |
|
|
}) |
|
|
|
|
|
import json |
|
|
from flask import Response |
|
|
return Response( |
|
|
json.dumps(data, ensure_ascii=False, indent=2), |
|
|
mimetype='application/json', |
|
|
headers={'Content-Disposition': 'attachment; filename=image_metadata.json'} |
|
|
) |
|
|
except Exception as e: |
|
|
return jsonify({'success': False, 'error': str(e)}) |
|
|
|
|
|
if __name__ == '__main__': |
|
|
app.run(host='0.0.0.0', port=7860, debug=False) |
|
|
|