Spaces:
Running
Running
File size: 15,577 Bytes
c001f24 d3d811e c001f24 d3d811e 2b5ce02 d3d811e 2b5ce02 d3d811e 2b5ce02 d3d811e 2b5ce02 d3d811e 2b5ce02 d3d811e 2b5ce02 d3d811e 2b5ce02 d3d811e 2b5ce02 d3d811e c001f24 d3d811e c001f24 2b5ce02 c001f24 d3d811e c001f24 d3d811e c001f24 2b5ce02 c001f24 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 |
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
from flask_login import login_required, current_user
from database import get_db_connection
import os
from flask import current_app
dashboard_bp = Blueprint('dashboard', __name__)
def get_session_size(session_id, user_id):
"""Calculate the total size of files associated with a session."""
import os
from flask import current_app
# Import logging
try:
from rich.console import Console
from rich.table import Table
console = Console()
rich_available = True
except ImportError:
# Rich not available, just use basic logging
console = None
rich_available = False
current_app.logger.info(f"Calculating size for session_id: {session_id}")
total_size = 0
breakdown = []
conn = get_db_connection()
# Get all images associated with the session
images = conn.execute("""
SELECT filename, processed_filename, image_type
FROM images
WHERE session_id = ?
""", (session_id,)).fetchall()
# Add sizes of original and processed images
for image in images:
# Add original file size (in upload folder)
if image['filename']:
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], image['filename'])
if os.path.exists(file_path):
size = os.path.getsize(file_path)
total_size += size
current_app.logger.info(f" Original image {image['filename']}: {size} bytes")
breakdown.append(("Original Image", image['filename'], size))
else:
current_app.logger.info(f" Original image file not found: {file_path}")
# Add processed/cropped image size (in processed folder)
if image['processed_filename']:
file_path = os.path.join(current_app.config['PROCESSED_FOLDER'], image['processed_filename'])
if os.path.exists(file_path):
size = os.path.getsize(file_path)
total_size += size
current_app.logger.info(f" Processed image {image['processed_filename']}: {size} bytes")
breakdown.append(("Processed Image", image['processed_filename'], size))
else:
current_app.logger.info(f" Processed image file not found: {file_path}")
# Add size of original PDF file if it exists
session_info = conn.execute("SELECT original_filename FROM sessions WHERE id = ?", (session_id,)).fetchone()
if session_info and session_info['original_filename']:
# Try to find the original PDF in the upload folder with the session ID prefix
pdf_filename = f"{session_id}_{session_info['original_filename']}"
pdf_path = os.path.join(current_app.config['UPLOAD_FOLDER'], pdf_filename)
if os.path.exists(pdf_path):
size = os.path.getsize(pdf_path)
total_size += size
current_app.logger.info(f" Original PDF {pdf_filename}: {size} bytes")
breakdown.append(("Original PDF", pdf_filename, size))
else:
current_app.logger.info(f" Original PDF file not found: {pdf_path}")
# Add size of any generated PDFs for this session
generated_pdfs = conn.execute("""
SELECT filename
FROM generated_pdfs
WHERE session_id = ?
""", (session_id,)).fetchall()
for pdf in generated_pdfs:
if pdf['filename']:
pdf_path = os.path.join(current_app.config['OUTPUT_FOLDER'], pdf['filename'])
if os.path.exists(pdf_path):
size = os.path.getsize(pdf_path)
total_size += size
current_app.logger.info(f" Generated PDF {pdf['filename']}: {size} bytes")
breakdown.append(("Generated PDF", pdf['filename'], size))
else:
current_app.logger.info(f" Generated PDF file not found: {pdf_path}")
current_app.logger.info(f"Total size for session {session_id}: {total_size} bytes")
# Create a rich table to show breakdown if rich is available
if rich_available and console:
table = Table(title=f"Session {session_id} Size Breakdown")
table.add_column("File Type", style="cyan")
table.add_column("Filename", style="magenta")
table.add_column("Size (bytes)", style="green")
for file_type, filename, size in breakdown:
table.add_row(file_type, filename, str(size))
if breakdown:
console.print(table)
else:
console.print(f"[yellow]No files found for session {session_id}[/yellow]")
conn.close()
return total_size
def format_file_size(size_bytes):
"""Convert bytes to human readable format."""
if size_bytes == 0:
return "0 B"
size_names = ["B", "KB", "MB", "GB"]
import math
i = int(math.floor(math.log(size_bytes, 1024)))
p = math.pow(1024, i)
s = round(size_bytes / p, 2)
return f"{s} {size_names[i]}"
@dashboard_bp.route('/dashboard')
@login_required
def dashboard():
# Check if size parameter is passed
show_size = request.args.get('size', type=int)
# Check filter parameter
filter_type = request.args.get('filter', 'all') # 'all', 'standard', 'collections'
conn = get_db_connection()
# Build the base query
if filter_type == 'collections':
# Only show neetprep collections
sessions_rows = conn.execute("""
SELECT s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type, s.group_name,
0 as page_count,
COUNT(nb.id) as question_count
FROM sessions s
LEFT JOIN neetprep_bookmarks nb ON s.id = nb.session_id
WHERE s.user_id = ? AND s.session_type = 'neetprep_collection'
GROUP BY s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type, s.group_name
ORDER BY s.created_at DESC
""", (current_user.id,)).fetchall()
elif filter_type == 'standard':
# Only show standard sessions (exclude collections and final_pdf)
sessions_rows = conn.execute("""
SELECT s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type, s.group_name,
COUNT(CASE WHEN i.image_type = 'original' THEN 1 END) as page_count,
COUNT(CASE WHEN i.image_type = 'cropped' THEN 1 END) as question_count
FROM sessions s
LEFT JOIN images i ON s.id = i.session_id
WHERE s.user_id = ? AND (s.session_type IS NULL OR s.session_type NOT IN ('final_pdf', 'neetprep_collection'))
GROUP BY s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type, s.group_name
ORDER BY s.created_at DESC
""", (current_user.id,)).fetchall()
else:
# Show all (both standard and collections, but not final_pdf)
# First get standard sessions
standard_sessions = conn.execute("""
SELECT s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type, s.group_name,
COUNT(CASE WHEN i.image_type = 'original' THEN 1 END) as page_count,
COUNT(CASE WHEN i.image_type = 'cropped' THEN 1 END) as question_count
FROM sessions s
LEFT JOIN images i ON s.id = i.session_id
WHERE s.user_id = ? AND (s.session_type IS NULL OR s.session_type NOT IN ('final_pdf', 'neetprep_collection'))
GROUP BY s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type, s.group_name
""", (current_user.id,)).fetchall()
# Then get neetprep collections
collection_sessions = conn.execute("""
SELECT s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type, s.group_name,
0 as page_count,
COUNT(nb.id) as question_count
FROM sessions s
LEFT JOIN neetprep_bookmarks nb ON s.id = nb.session_id
WHERE s.user_id = ? AND s.session_type = 'neetprep_collection'
GROUP BY s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type, s.group_name
""", (current_user.id,)).fetchall()
# Combine and sort by created_at
all_sessions = list(standard_sessions) + list(collection_sessions)
sessions_rows = sorted(all_sessions, key=lambda x: x['created_at'], reverse=True)
sessions = []
for session in sessions_rows:
session_dict = dict(session)
# Calculate total size for this session only if requested
if show_size:
session_size = get_session_size(session_dict['id'], current_user.id)
session_dict['total_size'] = session_size
session_dict['total_size_formatted'] = format_file_size(session_size)
sessions.append(session_dict)
conn.close()
return render_template('dashboard.html', sessions=sessions, show_size=bool(show_size), filter_type=filter_type)
@dashboard_bp.route('/sessions/update_group', methods=['POST'])
@login_required
def update_session_group():
data = request.json
session_id = data.get('session_id')
group_name = data.get('group_name')
if not session_id:
return jsonify({'error': 'Session ID is required'}), 400
# Sanitize group_name: empty string should be stored as NULL or empty
if group_name:
group_name = group_name.strip()
else:
group_name = None
try:
conn = get_db_connection()
# Security Check: Ensure the session belongs to the current user
session_owner = conn.execute('SELECT user_id FROM sessions WHERE id = ?', (session_id,)).fetchone()
if not session_owner or session_owner['user_id'] != current_user.id:
conn.close()
return jsonify({'error': 'Unauthorized'}), 403
conn.execute('UPDATE sessions SET group_name = ? WHERE id = ?', (group_name, session_id))
conn.commit()
conn.close()
return jsonify({'success': True})
except Exception as e:
return jsonify({'error': str(e)}), 500
@dashboard_bp.route('/sessions/batch_delete', methods=['POST'])
@login_required
def batch_delete_sessions():
data = request.json
session_ids = data.get('ids', [])
if not session_ids:
return jsonify({'error': 'No session IDs provided'}), 400
try:
conn = get_db_connection()
for session_id in session_ids:
# Security Check: Ensure the session belongs to the current user
session_info = conn.execute('SELECT user_id, session_type FROM sessions WHERE id = ?', (session_id,)).fetchone()
if not session_info or session_info['user_id'] != current_user.id:
# Silently skip or log an error, but don't delete
current_app.logger.warning(f"User {current_user.id} attempted to delete unauthorized session {session_id}.")
continue
# For bookmark collections, only delete bookmarks (not original images)
if session_info['session_type'] == 'neetprep_collection':
conn.execute('DELETE FROM neetprep_bookmarks WHERE session_id = ? AND user_id = ?', (session_id, current_user.id))
conn.execute('DELETE FROM sessions WHERE id = ?', (session_id,))
continue
# For regular sessions, delete associated files
images_to_delete = conn.execute('SELECT filename, processed_filename FROM images WHERE session_id = ?', (session_id,)).fetchall()
for img in images_to_delete:
if img['filename']:
try:
os.remove(os.path.join(current_app.config['UPLOAD_FOLDER'], img['filename']))
except OSError:
pass
if img['processed_filename']:
try:
os.remove(os.path.join(current_app.config['PROCESSED_FOLDER'], img['processed_filename']))
except OSError:
pass
# Delete from database
conn.execute('DELETE FROM questions WHERE session_id = ?', (session_id,))
conn.execute('DELETE FROM images WHERE session_id = ?', (session_id,))
conn.execute('DELETE FROM sessions WHERE id = ?', (session_id,))
conn.commit()
conn.close()
return jsonify({'success': True})
except Exception as e:
return jsonify({'error': str(e)}), 500
@dashboard_bp.route('/sessions/batch_update_group', methods=['POST'])
@login_required
def batch_update_session_group():
data = request.json
session_ids = data.get('ids', [])
group_name = data.get('group_name')
if not session_ids:
return jsonify({'error': 'No session IDs provided'}), 400
if group_name:
group_name = group_name.strip()
else:
group_name = None
try:
conn = get_db_connection()
for session_id in session_ids:
# Security Check
session_owner = conn.execute('SELECT user_id FROM sessions WHERE id = ?', (session_id,)).fetchone()
if session_owner and session_owner['user_id'] == current_user.id:
conn.execute('UPDATE sessions SET group_name = ? WHERE id = ?', (group_name, session_id))
conn.commit()
conn.close()
return jsonify({'success': True})
except Exception as e:
return jsonify({'error': str(e)}), 500
@dashboard_bp.route('/sessions/reduce_space/<session_id>', methods=['POST'])
@login_required
def reduce_space(session_id):
"""Truncate original page images to reduce disk space."""
try:
conn = get_db_connection()
# Security Check: Ensure the session belongs to the current user
session_owner = conn.execute('SELECT user_id FROM sessions WHERE id = ?', (session_id,)).fetchone()
if not session_owner or session_owner['user_id'] != current_user.id:
current_app.logger.warning(f"User {current_user.id} attempted to reduce space for unauthorized session {session_id}.")
return jsonify({'error': 'Unauthorized access to session'}), 403
# Get all original images associated with the session
images = conn.execute("""
SELECT filename
FROM images
WHERE session_id = ? AND image_type = 'original'
""", (session_id,)).fetchall()
# Truncate original images to reduce space
truncated_count = 0
for image in images:
if image['filename']:
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], image['filename'])
if os.path.exists(file_path):
try:
# Truncate the file to 0 bytes
with open(file_path, 'w') as f:
f.truncate(0)
truncated_count += 1
except OSError as e:
current_app.logger.error(f"Error truncating file {file_path}: {str(e)}")
conn.close()
return jsonify({
'success': True,
'truncated_count': truncated_count,
'message': f'Successfully reduced space by truncating {truncated_count} original page images'
})
except Exception as e:
current_app.logger.error(f"Error in reduce space: {str(e)}")
return jsonify({'error': str(e)}), 500
|