249 lines
7.5 KiB
Python
249 lines
7.5 KiB
Python
|
|
from flask import Blueprint, request, jsonify, send_file, current_app
|
||
|
|
from flask_login import login_required, current_user
|
||
|
|
from app import db
|
||
|
|
from app.models import Document
|
||
|
|
from werkzeug.utils import secure_filename
|
||
|
|
import os
|
||
|
|
import mimetypes
|
||
|
|
from datetime import datetime
|
||
|
|
|
||
|
|
bp = Blueprint('documents', __name__, url_prefix='/api/documents')
|
||
|
|
|
||
|
|
# Max file size: 10MB
|
||
|
|
MAX_FILE_SIZE = 10 * 1024 * 1024
|
||
|
|
|
||
|
|
# Allowed file types for documents
|
||
|
|
ALLOWED_DOCUMENT_TYPES = {
|
||
|
|
'pdf': 'application/pdf',
|
||
|
|
'csv': 'text/csv',
|
||
|
|
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
|
|
'xls': 'application/vnd.ms-excel',
|
||
|
|
'png': 'image/png',
|
||
|
|
'jpg': 'image/jpeg',
|
||
|
|
'jpeg': 'image/jpeg'
|
||
|
|
}
|
||
|
|
|
||
|
|
def allowed_document(filename):
|
||
|
|
"""Check if file type is allowed"""
|
||
|
|
return '.' in filename and \
|
||
|
|
filename.rsplit('.', 1)[1].lower() in ALLOWED_DOCUMENT_TYPES.keys()
|
||
|
|
|
||
|
|
def get_file_type_icon(file_type):
|
||
|
|
"""Get material icon name for file type"""
|
||
|
|
icons = {
|
||
|
|
'pdf': 'picture_as_pdf',
|
||
|
|
'csv': 'table_view',
|
||
|
|
'xlsx': 'table_view',
|
||
|
|
'xls': 'table_view',
|
||
|
|
'png': 'image',
|
||
|
|
'jpg': 'image',
|
||
|
|
'jpeg': 'image'
|
||
|
|
}
|
||
|
|
return icons.get(file_type.lower(), 'description')
|
||
|
|
|
||
|
|
@bp.route('/', methods=['GET'])
|
||
|
|
@login_required
|
||
|
|
def get_documents():
|
||
|
|
"""
|
||
|
|
Get all documents for current user
|
||
|
|
Security: Filters by current_user.id
|
||
|
|
"""
|
||
|
|
page = request.args.get('page', 1, type=int)
|
||
|
|
per_page = request.args.get('per_page', 10, type=int)
|
||
|
|
search = request.args.get('search', '')
|
||
|
|
|
||
|
|
# Security: Only get documents for current user
|
||
|
|
query = Document.query.filter_by(user_id=current_user.id)
|
||
|
|
|
||
|
|
if search:
|
||
|
|
query = query.filter(Document.original_filename.ilike(f'%{search}%'))
|
||
|
|
|
||
|
|
pagination = query.order_by(Document.created_at.desc()).paginate(
|
||
|
|
page=page, per_page=per_page, error_out=False
|
||
|
|
)
|
||
|
|
|
||
|
|
return jsonify({
|
||
|
|
'documents': [doc.to_dict() for doc in pagination.items],
|
||
|
|
'pagination': {
|
||
|
|
'page': page,
|
||
|
|
'pages': pagination.pages,
|
||
|
|
'total': pagination.total,
|
||
|
|
'per_page': per_page
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
|
||
|
|
@bp.route('/', methods=['POST'])
|
||
|
|
@login_required
|
||
|
|
def upload_document():
|
||
|
|
"""
|
||
|
|
Upload a new document
|
||
|
|
Security: Associates document with current_user.id
|
||
|
|
"""
|
||
|
|
if 'file' not in request.files:
|
||
|
|
return jsonify({'success': False, 'message': 'No file provided'}), 400
|
||
|
|
|
||
|
|
file = request.files['file']
|
||
|
|
|
||
|
|
if not file or not file.filename:
|
||
|
|
return jsonify({'success': False, 'message': 'No file selected'}), 400
|
||
|
|
|
||
|
|
if not allowed_document(file.filename):
|
||
|
|
return jsonify({
|
||
|
|
'success': False,
|
||
|
|
'message': 'Invalid file type. Allowed: PDF, CSV, XLS, XLSX, PNG, JPG'
|
||
|
|
}), 400
|
||
|
|
|
||
|
|
# Check file size
|
||
|
|
file.seek(0, os.SEEK_END)
|
||
|
|
file_size = file.tell()
|
||
|
|
file.seek(0)
|
||
|
|
|
||
|
|
if file_size > MAX_FILE_SIZE:
|
||
|
|
return jsonify({
|
||
|
|
'success': False,
|
||
|
|
'message': f'File too large. Maximum size: {MAX_FILE_SIZE // (1024*1024)}MB'
|
||
|
|
}), 400
|
||
|
|
|
||
|
|
# Generate secure filename
|
||
|
|
original_filename = secure_filename(file.filename)
|
||
|
|
file_ext = original_filename.rsplit('.', 1)[1].lower()
|
||
|
|
timestamp = datetime.utcnow().timestamp()
|
||
|
|
filename = f"{current_user.id}_{timestamp}_{original_filename}"
|
||
|
|
|
||
|
|
# Create documents directory if it doesn't exist
|
||
|
|
documents_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], 'documents')
|
||
|
|
os.makedirs(documents_dir, exist_ok=True)
|
||
|
|
|
||
|
|
# Save file
|
||
|
|
file_path = os.path.join(documents_dir, filename)
|
||
|
|
file.save(file_path)
|
||
|
|
|
||
|
|
# Get document category from form data
|
||
|
|
document_category = request.form.get('category', 'Other')
|
||
|
|
|
||
|
|
# Create document record - Security: user_id is current_user.id
|
||
|
|
document = Document(
|
||
|
|
filename=filename,
|
||
|
|
original_filename=original_filename,
|
||
|
|
file_path=file_path,
|
||
|
|
file_size=file_size,
|
||
|
|
file_type=file_ext.upper(),
|
||
|
|
mime_type=ALLOWED_DOCUMENT_TYPES.get(file_ext, 'application/octet-stream'),
|
||
|
|
document_category=document_category,
|
||
|
|
status='uploaded',
|
||
|
|
user_id=current_user.id
|
||
|
|
)
|
||
|
|
|
||
|
|
db.session.add(document)
|
||
|
|
db.session.commit()
|
||
|
|
|
||
|
|
return jsonify({
|
||
|
|
'success': True,
|
||
|
|
'message': 'Document uploaded successfully',
|
||
|
|
'document': document.to_dict()
|
||
|
|
}), 201
|
||
|
|
|
||
|
|
|
||
|
|
@bp.route('/<int:document_id>/view', methods=['GET'])
|
||
|
|
@login_required
|
||
|
|
def view_document(document_id):
|
||
|
|
"""
|
||
|
|
View/preview a document (inline, not download)
|
||
|
|
Security: Checks document belongs to current_user
|
||
|
|
"""
|
||
|
|
# Security: Filter by user_id
|
||
|
|
document = Document.query.filter_by(id=document_id, user_id=current_user.id).first()
|
||
|
|
|
||
|
|
if not document:
|
||
|
|
return jsonify({'success': False, 'message': 'Document not found'}), 404
|
||
|
|
|
||
|
|
if not os.path.exists(document.file_path):
|
||
|
|
return jsonify({'success': False, 'message': 'File not found on server'}), 404
|
||
|
|
|
||
|
|
return send_file(
|
||
|
|
document.file_path,
|
||
|
|
mimetype=document.mime_type,
|
||
|
|
as_attachment=False
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@bp.route('/<int:document_id>/download', methods=['GET'])
|
||
|
|
@login_required
|
||
|
|
def download_document(document_id):
|
||
|
|
"""
|
||
|
|
Download a document
|
||
|
|
Security: Checks document belongs to current_user
|
||
|
|
"""
|
||
|
|
# Security: Filter by user_id
|
||
|
|
document = Document.query.filter_by(id=document_id, user_id=current_user.id).first()
|
||
|
|
|
||
|
|
if not document:
|
||
|
|
return jsonify({'success': False, 'message': 'Document not found'}), 404
|
||
|
|
|
||
|
|
if not os.path.exists(document.file_path):
|
||
|
|
return jsonify({'success': False, 'message': 'File not found on server'}), 404
|
||
|
|
|
||
|
|
return send_file(
|
||
|
|
document.file_path,
|
||
|
|
mimetype=document.mime_type,
|
||
|
|
as_attachment=True,
|
||
|
|
download_name=document.original_filename
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@bp.route('/<int:document_id>', methods=['DELETE'])
|
||
|
|
@login_required
|
||
|
|
def delete_document(document_id):
|
||
|
|
"""
|
||
|
|
Delete a document
|
||
|
|
Security: Checks document belongs to current_user
|
||
|
|
"""
|
||
|
|
# Security: Filter by user_id
|
||
|
|
document = Document.query.filter_by(id=document_id, user_id=current_user.id).first()
|
||
|
|
|
||
|
|
if not document:
|
||
|
|
return jsonify({'success': False, 'message': 'Document not found'}), 404
|
||
|
|
|
||
|
|
# Delete physical file
|
||
|
|
if os.path.exists(document.file_path):
|
||
|
|
try:
|
||
|
|
os.remove(document.file_path)
|
||
|
|
except Exception as e:
|
||
|
|
print(f"Error deleting file: {e}")
|
||
|
|
|
||
|
|
# Delete database record
|
||
|
|
db.session.delete(document)
|
||
|
|
db.session.commit()
|
||
|
|
|
||
|
|
return jsonify({'success': True, 'message': 'Document deleted successfully'})
|
||
|
|
|
||
|
|
|
||
|
|
@bp.route('/<int:document_id>/status', methods=['PUT'])
|
||
|
|
@login_required
|
||
|
|
def update_document_status(document_id):
|
||
|
|
"""
|
||
|
|
Update document status (e.g., mark as analyzed)
|
||
|
|
Security: Checks document belongs to current_user
|
||
|
|
"""
|
||
|
|
# Security: Filter by user_id
|
||
|
|
document = Document.query.filter_by(id=document_id, user_id=current_user.id).first()
|
||
|
|
|
||
|
|
if not document:
|
||
|
|
return jsonify({'success': False, 'message': 'Document not found'}), 404
|
||
|
|
|
||
|
|
data = request.get_json()
|
||
|
|
new_status = data.get('status')
|
||
|
|
|
||
|
|
if new_status not in ['uploaded', 'processing', 'analyzed', 'error']:
|
||
|
|
return jsonify({'success': False, 'message': 'Invalid status'}), 400
|
||
|
|
|
||
|
|
document.status = new_status
|
||
|
|
db.session.commit()
|
||
|
|
|
||
|
|
return jsonify({
|
||
|
|
'success': True,
|
||
|
|
'message': 'Document status updated',
|
||
|
|
'document': document.to_dict()
|
||
|
|
})
|