feat(init): initial commit for Flask-based temporary file transfer service (web UI, API, SQLite)

This commit is contained in:
OpenCode Bot
2026-04-29 22:42:38 +08:00
commit f9f58ca64e
9 changed files with 434 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
*.db
uploads/
.flaskenv
.env

98
app.py Normal file
View File

@@ -0,0 +1,98 @@
import os
import uuid
from datetime import datetime
from flask import Flask, request, render_template, send_file, jsonify, url_for, abort
from config import UPLOAD_FOLDER, SECRET_KEY, MAX_CONTENT_LENGTH, EXPIRY_OPTIONS
from database import init_db, add_file, get_file, delete_file, cleanup_expired
app = Flask(__name__)
app.config['SECRET_KEY'] = SECRET_KEY
app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
init_db()
@app.route('/')
def index():
return render_template('index.html', expiry_options=EXPIRY_OPTIONS)
@app.route('/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return jsonify({'error': 'No file provided'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': 'No file selected'}), 400
expiry_key = request.form.get('expiry', '24h')
expiry_seconds = EXPIRY_OPTIONS.get(expiry_key, EXPIRY_OPTIONS['24h'])
expiry_hours = expiry_seconds // 3600
file_id = str(uuid.uuid4())
filename = file.filename
filepath = os.path.join(UPLOAD_FOLDER, file_id)
file.save(filepath)
filesize = os.path.getsize(filepath)
add_file(file_id, filename, filepath, filesize, expiry_hours)
share_url = url_for('download_file', file_id=file_id, _external=True)
return jsonify({'id': file_id, 'filename': filename, 'share_url': share_url})
@app.route('/api/upload', methods=['POST'])
def api_upload():
if 'file' not in request.files:
return jsonify({'error': 'No file provided'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': 'No file selected'}), 400
expiry_key = request.form.get('expiry', '24h')
expiry_seconds = EXPIRY_OPTIONS.get(expiry_key, EXPIRY_OPTIONS['24h'])
expiry_hours = expiry_seconds // 3600
file_id = str(uuid.uuid4())
filename = file.filename
filepath = os.path.join(UPLOAD_FOLDER, file_id)
file.save(filepath)
filesize = os.path.getsize(filepath)
add_file(file_id, filename, filepath, filesize, expiry_hours)
share_url = url_for('download_file', file_id=file_id, _external=True)
return jsonify({'id': file_id, 'filename': filename, 'filesize': filesize, 'expiry_hours': expiry_hours, 'share_url': share_url})
@app.route('/file/<file_id>')
def download_file(file_id):
cleanup_expired()
row = get_file(file_id)
if not row:
abort(404)
return render_template('download.html', file=row)
@app.route('/api/file/<file_id>')
def api_get_file(file_id):
cleanup_expired()
row = get_file(file_id)
if not row:
return jsonify({'error': 'File not found or expired'}), 404
return jsonify({
'id': row['id'],
'filename': row['filename'],
'filesize': row['filesize'],
'created_at': row['created_at'],
'expires_at': row['expires_at']
})
@app.route('/download/<file_id>')
def serve_file(file_id):
row = get_file(file_id)
if not row:
abort(404)
return send_file(row['filepath'], download_name=row['filename'], as_attachment=True)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)

14
config.py Normal file
View File

@@ -0,0 +1,14 @@
import os
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production')
UPLOAD_FOLDER = os.path.join(BASE_DIR, 'uploads')
DATABASE = os.path.join(BASE_DIR, 'files.db')
MAX_CONTENT_LENGTH = 500 * 1024 * 1024
EXPIRY_OPTIONS = {
'1h': 60 * 60,
'24h': 24 * 60 * 60,
'7d': 7 * 24 * 60 * 60,
}

61
database.py Normal file
View File

@@ -0,0 +1,61 @@
import sqlite3
import os
from datetime import datetime, timedelta
from config import DATABASE
def get_db():
conn = sqlite3.connect(DATABASE)
conn.row_factory = sqlite3.Row
return conn
def init_db():
os.makedirs(os.path.dirname(DATABASE) if os.path.dirname(DATABASE) else '.', exist_ok=True)
conn = get_db()
conn.execute('''
CREATE TABLE IF NOT EXISTS files (
id TEXT PRIMARY KEY,
filename TEXT NOT NULL,
filepath TEXT NOT NULL,
filesize INTEGER NOT NULL,
expiry_hours INTEGER NOT NULL,
created_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP NOT NULL
)
''')
conn.commit()
conn.close()
def add_file(file_id, filename, filepath, filesize, expiry_hours):
now = datetime.utcnow()
expires = now + timedelta(hours=expiry_hours)
conn = get_db()
conn.execute(
'INSERT INTO files (id, filename, filepath, filesize, expiry_hours, created_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
(file_id, filename, filepath, filesize, expiry_hours, now, expires)
)
conn.commit()
conn.close()
def get_file(file_id):
conn = get_db()
row = conn.execute('SELECT * FROM files WHERE id = ?', (file_id,)).fetchone()
conn.close()
return row
def delete_file(file_id):
conn = get_db()
conn.execute('DELETE FROM files WHERE id = ?', (file_id,))
conn.commit()
conn.close()
def cleanup_expired():
now = datetime.utcnow()
conn = get_db()
expired = conn.execute('SELECT * FROM files WHERE expires_at < ?', (now,)).fetchall()
for row in expired:
if os.path.exists(row['filepath']):
os.remove(row['filepath'])
conn.execute('DELETE FROM files WHERE expires_at < ?', (now,))
conn.commit()
conn.close()
return len(expired)

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
Flask>=3.0

40
setup-ssh.ps1 Normal file
View File

@@ -0,0 +1,40 @@
# PowerShell script to copy SSH public key to remote server
# Requires: PowerShell 5.1+ and .NET Framework
$remoteHost = "23.226.133.121"
$remotePort = "10022"
$remoteUser = "root"
$remotePassword = "xaj2h4v17CRYF52BUa"
$publicKeyPath = "$env:USERPROFILE\.ssh\id_rsa.pub"
# Read public key
$publicKey = Get-Content $publicKeyPath -Raw
# Use plink if available, otherwise provide manual instructions
$plink = Get-Command plink -ErrorAction SilentlyContinue
if ($plink) {
Write-Host "Using plink to copy public key..."
$cmd = "echo '$publicKey' | plink -P $remotePort -pw $remotePassword $remoteUser@$remoteHost `"mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys`""
Invoke-Expression $cmd
} else {
Write-Host "plink not found. Please manually add the public key to the remote server:"
Write-Host ""
Write-Host "1. Login to the remote server:"
Write-Host " ssh -p $remotePort $remoteUser@$remoteHost"
Write-Host " (Password: $remotePassword)"
Write-Host ""
Write-Host "2. Create .ssh directory if not exists:"
Write-Host " mkdir -p ~/.ssh && chmod 700 ~/.ssh"
Write-Host ""
Write-Host "3. Add the following public key to ~/.ssh/authorized_keys:"
Write-Host " (create the file if not exists, and chmod 600 ~/.ssh/authorized_keys)"
Write-Host ""
Write-Host "Public key content:"
Write-Host "=========================================="
Write-Host $publicKey
Write-Host "=========================================="
Write-Host ""
Write-Host "4. After adding, test with:"
Write-Host " ssh -p $remotePort $remoteUser@$remoteHost"
}

31
templates/download.html Normal file
View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ file.filename }} - 临时文件下载</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
.container { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); width: 100%; max-width: 500px; text-align: center; }
h1 { color: #333; margin-bottom: 20px; }
.file-info { background: #f8f9fa; padding: 20px; border-radius: 4px; margin: 20px 0; }
.file-info p { margin: 8px 0; color: #666; }
.file-info strong { color: #333; }
.expires { color: #dc3545; font-size: 14px; margin-top: 10px; }
a.download-btn { display: inline-block; padding: 12px 30px; background: #28a745; color: white; text-decoration: none; border-radius: 4px; font-size: 16px; }
a.download-btn:hover { background: #218838; }
</style>
</head>
<body>
<div class="container">
<h1>文件下载</h1>
<div class="file-info">
<p><strong>文件名:</strong>{{ file.filename }}</p>
<p><strong>大小:</strong>{{ (file.filesize / 1024 / 1024) | round(2) }} MB</p>
<p class="expires">过期时间:{{ file.expires_at }}</p>
</div>
<a href="{{ url_for('serve_file', file_id=file.id) }}" class="download-btn">下载文件</a>
</div>
</body>
</html>

124
templates/index.html Normal file
View File

@@ -0,0 +1,124 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>临时文件传输</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
.container { background: white; padding: 40px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); width: 100%; max-width: 500px; }
h1 { text-align: center; margin-bottom: 30px; color: #333; }
.upload-area { border: 2px dashed #ccc; border-radius: 8px; padding: 40px; text-align: center; cursor: pointer; transition: all 0.3s; }
.upload-area:hover, .upload-area.dragover { border-color: #007bff; background: #f0f8ff; }
.upload-area input { display: none; }
.upload-area p { color: #666; font-size: 16px; }
.expiry { margin: 20px 0; }
.expiry label { display: block; margin-bottom: 8px; color: #333; font-weight: 500; }
.expiry select { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; }
button { width: 100%; padding: 12px; background: #007bff; color: white; border: none; border-radius: 4px; font-size: 16px; cursor: pointer; }
button:hover { background: #0056b3; }
button:disabled { background: #ccc; cursor: not-allowed; }
.result { margin-top: 20px; padding: 15px; background: #d4edda; border-radius: 4px; display: none; }
.result a { color: #155724; word-break: break-all; }
.error { margin-top: 20px; padding: 15px; background: #f8d7da; border-radius: 4px; color: #721c24; display: none; }
</style>
</head>
<body>
<div class="container">
<h1>临时文件传输</h1>
<div class="upload-area" id="uploadArea">
<input type="file" id="fileInput">
<p>点击或拖拽文件到此处</p>
</div>
<div class="expiry">
<label>过期时间</label>
<select id="expiry">
{% for key, seconds in expiry_options.items() %}
<option value="{{ key }}">{% if key == '1h' %}1小时{% elif key == '24h' %}24小时{% else %}7天{% endif %}</option>
{% endfor %}
</select>
</div>
<button id="uploadBtn" onclick="uploadFile()">上传文件</button>
<div class="result" id="result">
<p>分享链接:<a href="" id="shareLink" target="_blank"></a></p>
</div>
<div class="error" id="error"></div>
</div>
<script>
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
uploadArea.addEventListener('click', () => fileInput.click());
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
if (e.dataTransfer.files.length) {
fileInput.files = e.dataTransfer.files;
uploadArea.querySelector('p').textContent = e.dataTransfer.files[0].name;
}
});
fileInput.addEventListener('change', () => {
if (fileInput.files.length) {
uploadArea.querySelector('p').textContent = fileInput.files[0].name;
}
});
function uploadFile() {
if (!fileInput.files.length) {
showError('请选择文件');
return;
}
const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('expiry', document.getElementById('expiry').value);
const btn = document.getElementById('uploadBtn');
btn.disabled = true;
btn.textContent = '上传中...';
fetch('/upload', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(data => {
if (data.error) {
showError(data.error);
} else {
const link = document.getElementById('shareLink');
link.href = data.share_url;
link.textContent = data.share_url;
document.getElementById('result').style.display = 'block';
document.getElementById('error').style.display = 'none';
}
})
.catch(err => showError('上传失败: ' + err.message))
.finally(() => {
btn.disabled = false;
btn.textContent = '上传文件';
});
}
function showError(msg) {
const errorDiv = document.getElementById('error');
errorDiv.textContent = msg;
errorDiv.style.display = 'block';
document.getElementById('result').style.display = 'none';
}
</script>
</body>
</html>

54
upload_client.py Normal file
View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""简易文件上传客户端,用于通过 API 上传文件到临时文件传输服务"""
import requests
import sys
import os
BASE_URL = "http://localhost:5000"
def upload_file(filepath, expiry='24h'):
"""
上传文件到临时文件传输服务
Args:
filepath: 要上传的文件路径
expiry: 过期时间,可选值: '1h', '24h', '7d'
Returns:
dict: 包含文件信息的字典
"""
if not os.path.exists(filepath):
raise FileNotFoundError(f"文件不存在: {filepath}")
url = f"{BASE_URL}/api/upload"
with open(filepath, 'rb') as f:
files = {'file': (os.path.basename(filepath), f)}
data = {'expiry': expiry}
response = requests.post(url, files=files, data=data)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"上传失败: {response.json().get('error', '未知错误')}")
if __name__ == '__main__':
if len(sys.argv) < 2:
print("用法: python upload_client.py <文件路径> [过期时间]")
print("过期时间可选: 1h, 24h, 7d (默认: 24h)")
sys.exit(1)
filepath = sys.argv[1]
expiry = sys.argv[2] if len(sys.argv) > 2 else '24h'
try:
result = upload_file(filepath, expiry)
print(f"上传成功!")
print(f"文件ID: {result['id']}")
print(f"文件名: {result['filename']}")
print(f"分享链接: {result['share_url']}")
if 'filesize' in result:
print(f"文件大小: {result['filesize']} 字节")
except Exception as e:
print(f"错误: {e}")
sys.exit(1)