Dnishe/blueprints/files.py
2026-06-08 21:56:14 +00:00

67 lines
2.5 KiB
Python

"""File upload and serving routes."""
import os
import secrets
from flask import Blueprint, jsonify, send_file, request, current_app
from werkzeug.utils import secure_filename
from database import get_db
from decorators import token_required
files_bp = Blueprint('files', __name__, url_prefix='/api')
MAX_UPLOAD_SIZE = 100 * 1024 * 1024
def process_file_upload(file, current_app):
"""Upload and store file with secure token-based filename."""
file_bytes = file.read()
size = len(file_bytes)
if size > MAX_UPLOAD_SIZE:
raise ValueError("File exceeds maximum allowed size.")
upload_dir = os.path.join(current_app.root_path, 'static', 'uploads')
os.makedirs(upload_dir, exist_ok=True)
orig = secure_filename(file.filename)
fname = f"{secrets.token_hex(8)}_{orig}"
path = os.path.join(upload_dir, fname)
with open(path, 'wb') as f:
f.write(file_bytes)
return {
'filename': file.filename,
'url': f'/api/files/{fname}',
'mimetype': file.mimetype,
'size': size
}
@files_bp.route('/files/<filename>', methods=['GET'])
@token_required
def serve_file(filename):
"""Serve uploaded files with secure headers to prevent execution/rendering."""
# Validate filename to prevent directory traversal
if '/' in filename or '\\' in filename or filename.startswith('.'):
return jsonify({'error': 'Invalid filename'}), 400
file_path = os.path.join(current_app.root_path, 'static', 'uploads', filename)
# Ensure file exists and is within uploads directory
if not os.path.exists(file_path) or not os.path.isfile(file_path):
return jsonify({'error': 'File not found'}), 404
# Verify the file is in the uploads directory
real_path = os.path.realpath(file_path)
uploads_dir = os.path.realpath(os.path.join(current_app.root_path, 'static', 'uploads'))
if not real_path.startswith(uploads_dir):
return jsonify({'error': 'Access denied'}), 403
# Serve file with secure headers
response = send_file(
file_path,
as_attachment=True, # Forces download instead of rendering
download_name=filename.split('_', 1)[-1] if '_' in filename else filename
)
# Additional security headers
response.headers['X-Content-Type-Options'] = 'nosniff' # Prevent MIME type sniffing
response.headers['X-Frame-Options'] = 'DENY' # Prevent clickjacking
response.headers['Content-Security-Policy'] = "default-src 'none'"
return response