1. app.py
python
import os
import base64
import hashlib
import json
import time
import uuid
from flask import Flask, request, render_template, send_file, jsonify, redirect, url_for
import qrcode
from cryptography.fernet import Fernet
app = Flask(__name__)
UPLOAD_FOLDER = 'uploads'
QR_CODE_FOLDER = 'qr_codes'
RECEIVED_FOLDER = 'received_files'
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['QR_CODE_FOLDER'] = QR_CODE_FOLDER
app.config['RECEIVED_FOLDER'] = RECEIVED_FOLDER
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(QR_CODE_FOLDER, exist_ok=True)
os.makedirs(RECEIVED_FOLDER, exist_ok=True)
transfer_sessions = {}
def calculate_sha256(file_path):
"""计算文件的 SHA-256 哈希值"""
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
def split_data(data, chunk_size):
"""将字节数据分割成指定大小的块"""
chunks = []
for i in range(0, len(data), chunk_size):
chunks.append(data[i:i + chunk_size])
return chunks
def generate_qr_code(data_str, filename):
"""生成二维码并保存为文件"""
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(data_str)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save(os.path.join(app.config['QR_CODE_FOLDER'], filename))
@app.route('/')
def index():
"""主页 - 文件上传"""
return render_template('index.html')
@app.route('/upload', methods=['POST'])
def upload_file():
"""处理文件上传和二维码生成"""
if 'files' not in request.files:
return "No file part", 400
files = request.files.getlist('files')
if not files or all(f.filename == '' for f in files):
return "No selected file", 400
try:
chunk_size_kb = int(request.form.get('chunk_size', 100))
display_speed = int(request.form.get('display_speed', 2))
except ValueError:
return "Invalid input for chunk size or display speed", 400
chunk_size_bytes = chunk_size_kb * 1024
delay_ms = int(1000 / display_speed) if display_speed > 0 else 500
session_id = str(uuid.uuid4())
key = Fernet.generate_key()
cipher_suite = Fernet(key)
uploaded_files_info = []
all_chunks_data = []
for file in files:
if file:
filename = file.filename
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
file_size = os.path.getsize(filepath)
file_hash = calculate_sha256(filepath)
file_type = file.content_type or 'unknown'
with open(filepath, 'rb') as f:
file_content = f.read()
encoded_content = base64.b64encode(file_content).decode('utf-8')
metadata = {
'filename': filename,
'size': file_size,
'type': file_type,
'sha256': file_hash
}
data_to_encrypt = json.dumps({
'metadata': metadata,
'content': encoded_content
}).encode('utf-8')
encrypted_data = cipher_suite.encrypt(data_to_encrypt)
chunks = split_data(encrypted_data, chunk_size_bytes)
all_chunks_data.extend([(chunk, filename) for chunk in chunks])
uploaded_files_info.append({
'name': filename,
'size': file_size,
'hash': file_hash
})
qr_filenames = []
total_chunks = len(all_chunks_data)
for i, (chunk_data, filename) in enumerate(all_chunks_data):
qr_data_dict = {
'session_id': session_id,
'index': i,
'total': total_chunks,
'filename': filename,
'data': base64.b64encode(chunk_data).decode('utf-8')
}
if i == 0:
qr_data_dict['key'] = key.decode('utf-8')
qr_data_str = json.dumps(qr_data_dict)
qr_filename = f"qr_{session_id}_{i}.png"
generate_qr_code(qr_data_str, qr_filename)
qr_filenames.append(qr_filename)
transfer_sessions[session_id] = {
'key': key.decode('utf-8'),
'total_chunks': total_chunks,
'chunks_received': {},
'files_info': uploaded_files_info
}
return render_template('index.html',
qr_files=qr_filenames,
delay_ms=delay_ms,
session_id=session_id,
files_info=uploaded_files_info)
@app.route('/scanner')
def scanner():
"""扫描页面"""
return render_template('scanner.html')
@app.route('/receive_chunk', methods=['POST'])
def receive_chunk():
"""接收扫描到的二维码数据块 (模拟)"""
data = request.json
qr_data_str = data.get('qr_data')
if not qr_data_str:
return jsonify({'status': 'error', 'message': 'No QR data received'}), 400
try:
qr_data = json.loads(qr_data_str)
except json.JSONDecodeError:
return jsonify({'status': 'error', 'message': 'Invalid QR data format'}), 400
session_id = qr_data.get('session_id')
index = qr_data.get('index')
total = qr_data.get('total')
filename = qr_data.get('filename')
encrypted_chunk_b64 = qr_data.get('data')
key_str = qr_data.get('key')
if not session_id or index is None or total is None or not encrypted_chunk_b64:
return jsonify({'status': 'error', 'message': 'Missing data in QR code'}), 400
if session_id not in transfer_sessions:
transfer_sessions[session_id] = {
'key': key_str,
'total_chunks': total,
'chunks_received': {},
'files_info': {}
}
elif key_str:
transfer_sessions[session_id]['key'] = key_str
session = transfer_sessions[session_id]
try:
cipher_suite = Fernet(session['key'].encode('utf-8'))
encrypted_chunk = base64.b64decode(encrypted_chunk_b64)
decrypted_chunk = cipher_suite.decrypt(encrypted_chunk)
chunk_data = json.loads(decrypted_chunk.decode('utf-8'))
session['chunks_received'][index] = {
'data': chunk_data,
'filename': filename
}
except Exception as e:
return jsonify({'status': 'error', 'message': f'Decryption failed: {str(e)}'}), 500
if len(session['chunks_received']) == session['total_chunks']:
try:
sorted_chunks = sorted(session['chunks_received'].items())
if sorted_chunks:
files_content = {}
for idx, chunk_info in sorted_chunks:
fname = chunk_info['filename']
content = chunk_info['data']['content']
if fname not in files_content:
files_content[fname] = []
files_content[fname].append(content)
for fname, parts in files_content.items():
full_base64_content = "".join(parts)
file_content = base64.b64decode(full_base64_content)
output_path = os.path.join(app.config['RECEIVED_FOLDER'], f"received_{fname}")
with open(output_path, 'wb') as f:
f.write(file_content)
del transfer_sessions[session_id]
return jsonify({'status': 'success', 'message': 'File(s) received and saved.', 'download_url': url_for('download_file', filename=f"received_{list(files_content.keys())[0]}")})
else:
return jsonify({'status': 'error', 'message': 'No chunks to assemble'}), 500
except Exception as e:
return jsonify({'status': 'error', 'message': f'File assembly failed: {str(e)}'}), 500
return jsonify({'status': 'success', 'message': f'Chunk {index+1}/{total} received'})
@app.route('/download/<filename>')
def download_file(filename):
"""提供下载重组后的文件"""
try:
return send_file(os.path.join(app.config['RECEIVED_FOLDER'], filename), as_attachment=True)
except FileNotFoundError:
return "File not found", 404
if __name__ == '__main__':
app.run(debug=True)
2. templates/index.html
html
<!DOCTYPE html>
<html>
<head>
<title>QR Code File Transfer - Sender</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
</head>
<body>
<h1>QR Code File Transfer - Upload</h1>
<form method="post" action="/upload" enctype="multipart/form-data">
<label for="files">Choose files (max 25):</label>
<input type="file" id="files" name="files" multiple required><br><br>
<label for="chunk_size">QR Code Chunk Size (KB):</label>
<input type="number" id="chunk_size" name="chunk_size" value="100" min="1"><br><br>
<label for="display_speed">Display Speed (QR codes/second):</label>
<input type="number" id="display_speed" name="display_speed" value="2" min="1"><br><br>
<input type="submit" value="Upload and Generate QR Codes">
</form>
{% if qr_files %}
<hr>
<h2>File Information:</h2>
<ul>
{% for file in files_info %}
<li>{{ file.name }} ({{ "%.2f" | format(file.size / 1024.0) }} KB, SHA256: {{ file.hash }})</li>
{% endfor %}
</ul>
<h2>Generated QR Codes:</h2>
<div id="slideshow-container">
{% for qr_file in qr_files %}
<div class="mySlides">
<img src="{{ url_for('static', filename='qr_codes/' + qr_file) }}" style="width:100%">
</div>
{% endfor %}
<a class="prev" onclick="plusSlides(-1)">❮</a>
<a class="next" onclick="plusSlides(1)">❯</a>
</div>
<br>
<div style="text-align:center">
{% for qr_file in qr_files %}
<span class="dot" onclick="currentSlide({{ loop.index }})"></span>
{% endfor %}
</div>
<h3>Auto-Play Slideshow:</h3>
<button onclick="startSlideshow()">Start</button>
<button onclick="stopSlideshow()">Stop</button>
<p>Speed: {{ delay_ms }} ms per slide</p>
<script>
let slideIndex = 1;
showSlides(slideIndex);
let slideInterval = null;
const delay = {{ delay_ms }};
function plusSlides(n) {
showSlides(slideIndex += n);
}
function currentSlide(n) {
showSlides(slideIndex = n);
}
function showSlides(n) {
let i;
let slides = document.getElementsByClassName("mySlides");
let dots = document.getElementsByClassName("dot");
if (n > slides.length) {slideIndex = 1}
if (n < 1) {slideIndex = slides.length}
for (i = 0; i < slides.length; i++) {
slides[i].style.display = "none";
}
for (i = 0; i < dots.length; i++) {
dots[i].className = dots[i].className.replace(" active", "");
}
slides[slideIndex-1].style.display = "block";
dots[slideIndex-1].className += " active";
}
function startSlideshow() {
if (slideInterval) stopSlideshow();
slideInterval = setInterval(() => {
plusSlides(1);
}, delay);
}
function stopSlideshow() {
clearInterval(slideInterval);
slideInterval = null;
}
</script>
{% endif %}
</body>
</html>
3. templates/scanner.html
html
<!DOCTYPE html>
<html>
<head>
<title>QR Code File Transfer - Receiver</title>
<style>
#video {
width: 100%;
max-width: 600px;
height: auto;
border: 1px solid black;
}
#result {
margin-top: 20px;
padding: 10px;
border: 1px solid #ccc;
background-color: #f9f9f9;
}
</style>
</head>
<body>
<h1>QR Code File Transfer - Receiver</h1>
<p>Point your camera at the QR codes being displayed on the sender's screen.</p>
<video id="video" width="300" height="200" autoplay muted></video>
<button id="startButton">Start Camera</button>
<button id="stopButton">Stop Camera</button>
<div id="result">
<p>Status: Waiting to start scanning...</p>
<p id="scanInfo">Scanned: 0 / 0</p>
<p id="speedInfo">Speed: 0 Kb/s</p>
<p id="message"></p>
</div>
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js"></script>
<script>
const video = document.getElementById('video');
const startButton = document.getElementById('startButton');
const stopButton = document.getElementById('stopButton');
const resultDiv = document.getElementById('result');
const scanInfo = document.getElementById('scanInfo');
const speedInfo = document.getElementById('speedInfo');
const messageP = document.getElementById('message');
let stream = null;
let scanning = false;
let lastScanTime = 0;
let lastScanCount = 0;
const constraints = {
video: {
facingMode: "environment",
width: { ideal: 1280 },
height: { ideal: 720 }
}
};
startButton.addEventListener('click', async () => {
try {
stream = await navigator.mediaDevices.getUserMedia(constraints);
video.srcObject = stream;
scanning = true;
lastScanTime = performance.now();
lastScanCount = 0;
scanContinuously();
} catch (err) {
console.error("Error accessing media devices:", err);
messageP.textContent = "Error accessing camera: " + err.message;
}
});
stopButton.addEventListener('click', () => {
scanning = false;
if (stream) {
const tracks = stream.getTracks();
tracks.forEach(track => track.stop());
video.srcObject = null;
}
messageP.textContent = "Camera stopped.";
});
function scanContinuously() {
if (!scanning) return;
const canvasElement = document.createElement('canvas');
const canvas = canvasElement.getContext('2d');
canvasElement.width = video.videoWidth;
canvasElement.height = video.videoHeight;
canvas.drawImage(video, 0, 0, canvasElement.width, canvasElement.height);
const imageData = canvas.getImageData(0, 0, canvasElement.width, canvasElement.height);
const code = jsQR(imageData.data, imageData.width, imageData.height, {
inversionAttempts: "dontInvert",
});
if (code) {
console.log("Scanned QR Code Data:", code.data);
fetch('/receive_chunk', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ qr_data: code.data })
})
.then(response => response.json())
.then(data => {
console.log('Success:', data);
messageP.textContent = data.message || "Chunk processed.";
if (data.status === 'success' && data.download_url) {
messageP.innerHTML += ` <a href="${data.download_url}" download>Download File</a>`;
}
lastScanCount++;
const now = performance.now();
const elapsedSeconds = (now - lastScanTime) / 1000;
if (elapsedSeconds > 1) {
const speedKbps = (lastScanCount * 100 ) / elapsedSeconds;
speedInfo.textContent = `Speed: ${speedKbps.toFixed(2)} Kb/s`;
lastScanTime = now;
lastScanCount = 0;
}
scanInfo.textContent = `Scanned: ${lastScanCount} (est.)`;
})
.catch((error) => {
console.error('Error:', error);
messageP.textContent = "Error sending " + error;
});
}
requestAnimationFrame(scanContinuously);
}
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
messageP.textContent = "Your browser does not support camera access.";
startButton.disabled = true;
}
if (!window.jsQR) {
messageP.textContent = "QR code scanning library (jsQR) failed to load.";
startButton.disabled = true;
}
</script>
</body>
</html>
4. static/styles.css
css
#slideshow-container {
max-width: 1000px;
position: relative;
margin: auto;
}
.mySlides {
display: none;
}
.prev, .next {
cursor: pointer;
position: absolute;
top: 50%;
width: auto;
padding: 16px;
margin-top: -22px;
color: white;
font-weight: bold;
font-size: 18px;
transition: 0.6s ease;
border-radius: 0 3px 3px 0;
user-select: none;
}
.next {
right: 0;
border-radius: 3px 0 0 3px;
}
.prev:hover, .next:hover {
background-color: rgba(0,0,0,0.8);
}
.text {
color: #f2f2f2;
font-size: 15px;
padding: 8px 12px;
position: absolute;
bottom: 8px;
width: 100%;
text-align: center;
}
.numbertext {
color: #f2f2f2;
font-size: 12px;
padding: 8px 12px;
position: absolute;
top: 0;
}
.dot {
cursor: pointer;
height: 15px;
width: 15px;
margin: 0 2px;
background-color: #bbb;
border-radius: 50%;
display: inline-block;
transition: background-color 0.6s ease;
}
.active, .dot:hover {
background-color: #717171;
}
.fade {
animation-name: fade;
animation-duration: 1.5s;
}
@keyframes fade {
from {opacity: .4}
to {opacity: 1}
}