commit f9f58ca64e019d08c9989de86c573c73c0746867 Author: OpenCode Bot Date: Wed Apr 29 22:42:38 2026 +0800 feat(init): initial commit for Flask-based temporary file transfer service (web UI, API, SQLite) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..15f12df --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +.Python +env/ +venv/ +*.db +uploads/ +.flaskenv +.env diff --git a/app.py b/app.py new file mode 100644 index 0000000..7a75657 --- /dev/null +++ b/app.py @@ -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/') +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/') +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/') +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) diff --git a/config.py b/config.py new file mode 100644 index 0000000..cc7efec --- /dev/null +++ b/config.py @@ -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, +} diff --git a/database.py b/database.py new file mode 100644 index 0000000..8372439 --- /dev/null +++ b/database.py @@ -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) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c37b670 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Flask>=3.0 diff --git a/setup-ssh.ps1 b/setup-ssh.ps1 new file mode 100644 index 0000000..0e17f59 --- /dev/null +++ b/setup-ssh.ps1 @@ -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" +} diff --git a/templates/download.html b/templates/download.html new file mode 100644 index 0000000..04803c0 --- /dev/null +++ b/templates/download.html @@ -0,0 +1,31 @@ + + + + + + {{ file.filename }} - 临时文件下载 + + + +
+

文件下载

+
+

文件名:{{ file.filename }}

+

大小:{{ (file.filesize / 1024 / 1024) | round(2) }} MB

+

过期时间:{{ file.expires_at }}

+
+ 下载文件 +
+ + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..b8ea04b --- /dev/null +++ b/templates/index.html @@ -0,0 +1,124 @@ + + + + + + 临时文件传输 + + + +
+

临时文件传输

+
+ +

点击或拖拽文件到此处

+
+
+ + +
+ +
+

分享链接:

+
+
+
+ + + + diff --git a/upload_client.py b/upload_client.py new file mode 100644 index 0000000..0991c74 --- /dev/null +++ b/upload_client.py @@ -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)