imgur-hf / app.py
Norcoo's picture
Upload 2 files
16642de verified
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", "")
# Flask应用
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模板
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">&times;</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
# 生成hash和文件名
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)