feat(init): initial commit for Flask-based temporary file transfer service (web UI, API, SQLite)
This commit is contained in:
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
.Python
|
||||
env/
|
||||
venv/
|
||||
*.db
|
||||
uploads/
|
||||
.flaskenv
|
||||
.env
|
||||
98
app.py
Normal file
98
app.py
Normal 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
14
config.py
Normal 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
61
database.py
Normal 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
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
Flask>=3.0
|
||||
40
setup-ssh.ps1
Normal file
40
setup-ssh.ps1
Normal 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
31
templates/download.html
Normal 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
124
templates/index.html
Normal 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
54
upload_client.py
Normal 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)
|
||||
Reference in New Issue
Block a user