From b9a82af12fa063e2c6ad9d6a41ae0ffb6b3568ef Mon Sep 17 00:00:00 2001 From: pika Date: Sun, 23 Mar 2025 01:31:21 +0100 Subject: [PATCH] working folder creation --- app/__init__.py | 221 +++- app/models.py | 49 +- app/routes/files.py | 1054 ++++++++--------- app/static/js/common.js | 162 +++ app/templates/auth/profile.html | 712 ++++++++--- app/templates/auth/settings.html | 436 +++++++ app/templates/base.html | 62 +- app/templates/files/browser.html | 796 +++++++++---- .../files/partials/folder_contents.html | 17 +- app/templates/files/upload.html | 750 ++++-------- template_checker.py | 84 ++ 11 files changed, 2791 insertions(+), 1552 deletions(-) create mode 100644 app/static/js/common.js create mode 100644 app/templates/auth/settings.html create mode 100644 template_checker.py diff --git a/app/__init__.py b/app/__init__.py index 1ad2dbd..8c5e09f 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -5,6 +5,7 @@ from config import Config import os from datetime import datetime import sqlite3 +import logging # Initialize extensions db = SQLAlchemy() @@ -13,61 +14,185 @@ login_manager.login_view = 'auth.login' login_manager.login_message_category = 'info' def initialize_database(app): - """Create database tables if they don't exist""" + """Create and initialize database tables if they don't exist""" with app.app_context(): + app.logger.info("Initializing database...") try: - # Create all tables + # Create all tables (this is safe to call even if tables exist) db.create_all() - app.logger.info("Database tables created successfully") + + # Check if we need to add the Share and Download models + inspector = db.inspect(db.engine) + if not inspector.has_table('share'): + app.logger.info("Creating Share table...") + # Import models to ensure they're registered with SQLAlchemy + from app.models import Share + if not inspector.has_table('share'): + # Create the Share table + Share.__table__.create(db.engine) + + if not inspector.has_table('download'): + app.logger.info("Creating Download table...") + from app.models import Download + if not inspector.has_table('download'): + # Create the Download table + Download.__table__.create(db.engine) + + # Check for existing users - create admin if none + from app.models import User + if User.query.count() == 0: + app.logger.info("No users found, creating default admin user...") + admin = User(username='admin', email='admin@example.com') + admin.set_password('adminpassword') + db.session.add(admin) + db.session.commit() + app.logger.info("Default admin user created") + + app.logger.info("Database initialization complete") except Exception as e: - app.logger.error(f"Error creating database tables: {str(e)}") + app.logger.error(f"Error initializing database: {str(e)}") + # Don't raise the exception to prevent app startup failure + # But log it for debugging purposes def run_migrations(app): - """Apply any necessary database migrations""" - db_path = app.config['SQLALCHEMY_DATABASE_URI'].replace('sqlite:///', '') - - if not os.path.exists(db_path): - app.logger.info(f"Database file does not exist: {db_path}") - return - - try: - conn = sqlite3.connect(db_path) - cursor = conn.cursor() - - # Check if storage_name column exists in file table - cursor.execute("PRAGMA table_info(file)") - columns = [column[1] for column in cursor.fetchall()] - - if 'storage_name' not in columns: - app.logger.info("Adding storage_name column to file table") - cursor.execute("ALTER TABLE file ADD COLUMN storage_name TEXT") + """Apply any necessary database migrations automatically""" + with app.app_context(): + try: + app.logger.info("Running database migrations...") + # Get database path + db_path = app.config['SQLALCHEMY_DATABASE_URI'].replace('sqlite:///', '') - # Update existing records to use filename as storage_name - cursor.execute("UPDATE file SET storage_name = name WHERE storage_name IS NULL AND is_folder = 0") - conn.commit() - - conn.close() - app.logger.info("Database migrations completed successfully") - except sqlite3.OperationalError as e: - if "no such table: file" in str(e): - app.logger.info("File table doesn't exist yet, will be created with db.create_all()") - else: - app.logger.error(f"Error during migration: {str(e)}") - except Exception as e: - app.logger.error(f"Error during migration: {str(e)}") + # Check if we're using SQLite and if the database file exists + if db_path.startswith('/'): # Absolute path + if not os.path.exists(db_path): + app.logger.info(f"Database file does not exist: {db_path}") + return + + # Use SQLAlchemy to check and add missing columns + inspector = db.inspect(db.engine) + + # Check for 'file' table columns + if inspector.has_table('file'): + columns = [col['name'] for col in inspector.get_columns('file')] + + # Add storage_name column if it doesn't exist + if 'storage_name' not in columns: + app.logger.info("Adding storage_name column to file table") + if db.engine.name == 'sqlite': + # For SQLite, use direct SQL as it doesn't support ALTER TABLE ADD COLUMN with default + db.session.execute('ALTER TABLE file ADD COLUMN storage_name TEXT') + # Update existing records + db.session.execute('UPDATE file SET storage_name = uuid() WHERE storage_name IS NULL AND is_folder = 0') + db.session.commit() + + # Check for user table columns + if inspector.has_table('user'): + columns = [col['name'] for col in inspector.get_columns('user')] + + # Add last_login column if it doesn't exist + if 'last_login' not in columns: + app.logger.info("Adding last_login column to user table") + if db.engine.name == 'sqlite': + db.session.execute('ALTER TABLE user ADD COLUMN last_login DATETIME') + db.session.commit() + + app.logger.info("Database migrations complete") + except Exception as e: + app.logger.error(f"Error during database migration: {str(e)}") + # Log error but don't crash the app + +def get_file_icon(mime_type, filename): + """Return Font Awesome icon class based on file type""" + if mime_type: + if mime_type.startswith('image/'): + return 'fa-file-image' + elif mime_type.startswith('video/'): + return 'fa-file-video' + elif mime_type.startswith('audio/'): + return 'fa-file-audio' + elif mime_type.startswith('text/'): + return 'fa-file-alt' + elif mime_type.startswith('application/pdf'): + return 'fa-file-pdf' + elif 'spreadsheet' in mime_type or 'excel' in mime_type: + return 'fa-file-excel' + elif 'presentation' in mime_type or 'powerpoint' in mime_type: + return 'fa-file-powerpoint' + elif 'document' in mime_type or 'word' in mime_type: + return 'fa-file-word' + + # Check by extension + ext = os.path.splitext(filename)[1].lower()[1:] + if ext in ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp']: + return 'fa-file-image' + elif ext in ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv']: + return 'fa-file-video' + elif ext in ['mp3', 'wav', 'ogg', 'flac', 'm4a']: + return 'fa-file-audio' + elif ext in ['doc', 'docx', 'odt']: + return 'fa-file-word' + elif ext in ['xls', 'xlsx', 'ods', 'csv']: + return 'fa-file-excel' + elif ext in ['ppt', 'pptx', 'odp']: + return 'fa-file-powerpoint' + elif ext == 'pdf': + return 'fa-file-pdf' + elif ext in ['zip', 'rar', '7z', 'tar', 'gz']: + return 'fa-file-archive' + elif ext in ['txt', 'rtf', 'md']: + return 'fa-file-alt' + elif ext in ['html', 'css', 'js', 'py', 'java', 'php', 'c', 'cpp', 'json', 'xml']: + return 'fa-file-code' + + return 'fa-file' + +def format_file_size(size): + """Format file size in bytes to human-readable format""" + if not size: + return "0 B" + size_names = ("B", "KB", "MB", "GB", "TB") + i = 0 + while size >= 1024 and i < len(size_names) - 1: + size /= 1024 + i += 1 + return f"{size:.1f} {size_names[i]}" + def create_app(config_class=Config): app = Flask(__name__) app.config.from_object(config_class) + # Configure logging + if not app.debug: + # Set up file handler + if not os.path.exists('logs'): + os.mkdir('logs') + file_handler = logging.FileHandler('logs/flask_files.log') + file_handler.setFormatter(logging.Formatter( + '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]' + )) + file_handler.setLevel(logging.INFO) + app.logger.addHandler(file_handler) + + # Set log level + app.logger.setLevel(logging.INFO) + app.logger.info('Flask Files startup') + # Initialize extensions db.init_app(app) login_manager.init_app(app) # Initialize the upload folder - os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) + upload_folder = app.config.get('UPLOAD_FOLDER', 'uploads') + if not os.path.isabs(upload_folder): + # If it's a relative path, make it relative to the app instance folder + upload_folder = os.path.join(app.instance_path, upload_folder) + app.config['UPLOAD_FOLDER'] = upload_folder - # Auto initialize database if it doesn't exist + os.makedirs(upload_folder, exist_ok=True) + app.logger.info(f"Upload folder initialized at: {upload_folder}") + + # Auto initialize database and run migrations on startup with app.app_context(): initialize_database(app) run_migrations(app) @@ -83,9 +208,27 @@ def create_app(config_class=Config): # Add context processor for template variables @app.context_processor - def inject_now(): - return {'now': datetime.now()} + def inject_global_variables(): + return { + 'now': datetime.now(), + 'file_icon': get_file_icon, + 'format_size': format_file_size, + 'app_version': '1.0.0', # Add version number for caching + } + + # Handle 404 errors + @app.errorhandler(404) + def not_found_error(error): + return render_template('errors/404.html'), 404 + + # Handle 500 errors + @app.errorhandler(500) + def internal_error(error): + db.session.rollback() # Rollback any failed database transactions + return render_template('errors/500.html'), 500 return app +# Import must come after create_app to avoid circular imports +from flask import render_template # For error handlers from app import models diff --git a/app/models.py b/app/models.py index bdc9ea7..5793953 100644 --- a/app/models.py +++ b/app/models.py @@ -21,6 +21,10 @@ class User(UserMixin, db.Model): # Relationships files = db.relationship('File', backref='owner', lazy='dynamic', foreign_keys='File.user_id', cascade='all, delete-orphan') + shares = db.relationship('Share', backref='owner', lazy='dynamic', + foreign_keys='Share.user_id', cascade='all, delete-orphan') + downloads = db.relationship('Download', backref='user', lazy='dynamic', + foreign_keys='Download.user_id', cascade='all, delete-orphan') def __repr__(self): return f'' @@ -48,6 +52,8 @@ class File(db.Model): # Relationships children = db.relationship('File', backref=db.backref('parent', remote_side=[id]), lazy='dynamic', cascade='all, delete-orphan') + shares = db.relationship('Share', backref='file', lazy='dynamic', cascade='all, delete-orphan') + downloads = db.relationship('Download', backref='file', lazy='dynamic', cascade='all, delete-orphan') def __repr__(self): return f'' @@ -62,17 +68,42 @@ class File(db.Model): return f"{uuid.uuid4().hex}.{ext}" if ext else f"{uuid.uuid4().hex}" class Share(db.Model): + __tablename__ = 'share' + id = db.Column(db.Integer, primary_key=True) - file_id = db.Column(db.Integer, db.ForeignKey('file.id')) - user_id = db.Column(db.Integer, db.ForeignKey('user.id')) - token = db.Column(db.String(64), unique=True) + file_id = db.Column(db.Integer, db.ForeignKey('file.id', ondelete='CASCADE'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) + share_token = db.Column(db.String(64), unique=True) + is_public = db.Column(db.Boolean, default=False) + is_password_protected = db.Column(db.Boolean, default=False) + password_hash = db.Column(db.String(128)) + expiry_date = db.Column(db.DateTime, nullable=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) - expires_at = db.Column(db.DateTime, nullable=True) - downloads = db.relationship('Download', backref='share', lazy='dynamic') + + def __repr__(self): + return f'' + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + self.is_password_protected = True + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + + def generate_token(self): + """Generate a unique token for sharing""" + return uuid.uuid4().hex class Download(db.Model): + __tablename__ = 'download' + id = db.Column(db.Integer, primary_key=True) - file_id = db.Column(db.Integer, db.ForeignKey('file.id')) - share_id = db.Column(db.Integer, db.ForeignKey('share.id')) - ip_address = db.Column(db.String(45)) - timestamp = db.Column(db.DateTime, default=datetime.utcnow) + file_id = db.Column(db.Integer, db.ForeignKey('file.id', ondelete='CASCADE'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True) # Nullable for anonymous downloads + ip_address = db.Column(db.String(64)) + user_agent = db.Column(db.String(255)) + share_id = db.Column(db.Integer, db.ForeignKey('share.id', ondelete='SET NULL'), nullable=True) + downloaded_at = db.Column(db.DateTime, default=datetime.utcnow) + + def __repr__(self): + return f'' diff --git a/app/routes/files.py b/app/routes/files.py index 86ed353..4ce413b 100644 --- a/app/routes/files.py +++ b/app/routes/files.py @@ -1,146 +1,178 @@ -from flask import render_template, redirect, url_for, flash, request, send_from_directory, abort, jsonify, send_file +from flask import Blueprint, render_template, redirect, url_for, flash, request, send_from_directory, abort, jsonify, send_file, current_app from flask_login import login_required, current_user from werkzeug.utils import secure_filename from app import db from app.models import File, Share -from app.routes import files_bp +from config import Config import os from datetime import datetime, timedelta import uuid import mimetypes -from config import Config import shutil +import json +files_bp = Blueprint('files', __name__, url_prefix='/files') + +@files_bp.route('/') @files_bp.route('/browser') @files_bp.route('/browser/') @login_required def browser(folder_id=None): - # Get current folder + """Display file browser interface""" current_folder = None - if folder_id: - current_folder = File.query.filter_by(id=folder_id, user_id=current_user.id).first_or_404() - if not current_folder.is_folder: - abort(400) - - # Generate breadcrumb navigation breadcrumbs = [] - if current_folder: - temp_folder = current_folder - while temp_folder: - breadcrumbs.append(temp_folder) - temp_folder = temp_folder.parent + + if folder_id: + current_folder = File.query.filter_by(id=folder_id, user_id=current_user.id, is_folder=True).first_or_404() + + # Generate breadcrumbs + breadcrumbs = [] + parent = current_folder + while parent: + breadcrumbs.append(parent) + parent = parent.parent breadcrumbs.reverse() - # Get files and folders - query = File.query.filter_by(user_id=current_user.id, parent_id=folder_id) - folders = query.filter_by(is_folder=True).order_by(File.name).all() - files = query.filter_by(is_folder=False).order_by(File.name).all() - - return render_template('files/browser.html', - title='File Browser', - current_folder=current_folder, - breadcrumbs=breadcrumbs, - folders=folders, - files=files) - -@files_bp.route('/upload') -@login_required -def upload(): - folder_id = request.args.get('folder', None, type=int) - - # Get parent folder - parent_folder = None - if folder_id: - parent_folder = File.query.filter_by(id=folder_id, user_id=current_user.id, is_folder=True).first_or_404() - - return render_template('files/upload.html', - title='Upload Files', - parent_folder=parent_folder) - -@files_bp.route('/upload_folder', methods=['POST']) -@login_required -def upload_folder(): - folder_id = request.form.get('folder_id', None, type=int) - - # Get parent folder - parent_folder = None - if folder_id: - parent_folder = File.query.filter_by(id=folder_id, user_id=current_user.id, is_folder=True).first_or_404() - - # Create temporary directory for uploaded files - temp_dir = os.path.join(Config.UPLOAD_FOLDER, 'temp', str(uuid.uuid4())) - os.makedirs(temp_dir, exist_ok=True) - - try: - # Process uploaded files (with relative paths) - files = request.files.getlist('folder[]') - if not files or all(file.filename == '' for file in files): - flash('No folder selected for upload', 'error') - shutil.rmtree(temp_dir, ignore_errors=True) - return redirect(url_for('files.browser', folder_id=folder_id)) + # For initial load - only get folders and files if it's not an AJAX request + if not request.headers.get('X-Requested-With') == 'XMLHttpRequest': + if current_folder: + folders = File.query.filter_by(parent_id=current_folder.id, user_id=current_user.id, is_folder=True).all() + files = File.query.filter_by(parent_id=current_folder.id, user_id=current_user.id, is_folder=False).all() + else: + folders = File.query.filter_by(parent_id=None, user_id=current_user.id, is_folder=True).all() + files = File.query.filter_by(parent_id=None, user_id=current_user.id, is_folder=False).all() - # Save files to temp directory with their relative paths - for file in files: + return render_template('files/browser.html', + current_folder=current_folder, + breadcrumbs=breadcrumbs, + folders=folders, + files=files) + else: + # If it's an AJAX request, return JSON + return jsonify({'error': 'Use the /contents endpoint for AJAX requests'}) + +@files_bp.route('/contents') +@login_required +def folder_contents(): + """Returns the HTML for folder contents (used for AJAX loading)""" + folder_id = request.args.get('folder_id', type=int) + sort_by = request.args.get('sort', 'name') + sort_order = request.args.get('order', 'asc') + search_query = request.args.get('search', '') + + # Get the current folder if a folder_id is provided + current_folder = None + if folder_id: + current_folder = File.query.filter_by(id=folder_id, user_id=current_user.id, is_folder=True).first_or_404() + + # Base query for folders and files + folders_query = File.query.filter_by(user_id=current_user.id, is_folder=True) + files_query = File.query.filter_by(user_id=current_user.id, is_folder=False) + + # Filter by parent folder + if current_folder: + folders_query = folders_query.filter_by(parent_id=current_folder.id) + files_query = files_query.filter_by(parent_id=current_folder.id) + else: + folders_query = folders_query.filter_by(parent_id=None) + files_query = files_query.filter_by(parent_id=None) + + # Apply search if provided + if search_query: + folders_query = folders_query.filter(File.name.ilike(f'%{search_query}%')) + files_query = files_query.filter(File.name.ilike(f'%{search_query}%')) + + # Apply sorting + if sort_by == 'name': + folders_query = folders_query.order_by(File.name.asc() if sort_order == 'asc' else File.name.desc()) + files_query = files_query.order_by(File.name.asc() if sort_order == 'asc' else File.name.desc()) + elif sort_by == 'date': + folders_query = folders_query.order_by(File.updated_at.desc() if sort_order == 'asc' else File.updated_at.asc()) + files_query = files_query.order_by(File.updated_at.desc() if sort_order == 'asc' else File.updated_at.asc()) + elif sort_by == 'size': + # Folders always come first, then sort files by size + files_query = files_query.order_by(File.size.asc() if sort_order == 'asc' else File.size.desc()) + + # Get the results + folders = folders_query.all() + files = files_query.all() + + # Different format of response based on requested format + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + # If AJAX request, check if JSON or HTML is requested + if request.args.get('format') == 'json': + # Return JSON response with folder and file data + folder_data = [{ + 'id': folder.id, + 'name': folder.name, + 'updated_at': folder.updated_at.isoformat(), + 'is_folder': True, + 'item_count': folder.children.count(), + 'url': url_for('files.browser', folder_id=folder.id) + } for folder in folders] + + file_data = [{ + 'id': file.id, + 'name': file.name, + 'size': file.size, + 'formatted_size': format_file_size(file.size), + 'mime_type': file.mime_type, + 'updated_at': file.updated_at.isoformat(), + 'is_folder': False, + 'icon': get_file_icon(file.mime_type, file.name), + 'url': url_for('files.download', file_id=file.id) + } for file in files] + + return jsonify({ + 'folders': folder_data, + 'files': file_data, + 'total_items': len(folder_data) + len(file_data) + }) + else: + # Return HTML partial + return render_template('files/partials/folder_contents.html', + current_folder=current_folder, + folders=folders, + files=files) + else: + # If not AJAX, redirect to browser view + if current_folder: + return redirect(url_for('files.browser', folder_id=current_folder.id)) + else: + return redirect(url_for('files.browser')) + +@files_bp.route('/upload', methods=['GET', 'POST']) +@files_bp.route('/upload/', methods=['GET', 'POST']) +@login_required +def upload(folder_id=None): + """Page for uploading files""" + parent_folder = None + if folder_id: + parent_folder = File.query.filter_by(id=folder_id, user_id=current_user.id, is_folder=True).first_or_404() + + if request.method == 'POST': + # Handle XHR upload + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + # Check if file was included + if 'file' not in request.files: + return jsonify({'error': 'No file part'}), 400 + + file = request.files['file'] + + # Check if the file was actually selected if file.filename == '': - continue - - # Get the relative path within the folder - rel_path = file.filename - if '\\' in rel_path: # Windows paths - rel_path = rel_path.replace('\\', '/') + return jsonify({'error': 'No selected file'}), 400 - # Create directories for the path - dir_path = os.path.join(temp_dir, os.path.dirname(rel_path)) - os.makedirs(dir_path, exist_ok=True) + # Validate and save file + filename = secure_filename(file.filename) - # Save the file - file.save(os.path.join(temp_dir, rel_path)) - - # Process the folder structure - base_folder_name = os.path.basename(os.path.normpath(temp_dir)) - for root, dirs, files in os.walk(temp_dir): - rel_root = os.path.relpath(root, temp_dir) + # Generate UUID for storage + file_uuid = str(uuid.uuid4()) + storage_path = os.path.join(Config.UPLOAD_FOLDER, file_uuid) - # Skip the root directory itself - if rel_root == '.': - rel_root = '' - - # Find or create parent folder - current_parent_id = folder_id - if rel_root: - path_parts = rel_root.split(os.path.sep) - for part in path_parts: - existing_folder = File.query.filter_by( - name=part, - parent_id=current_parent_id, - user_id=current_user.id, - is_folder=True - ).first() - - if existing_folder: - current_parent_id = existing_folder.id - else: - new_folder = File( - name=part, - user_id=current_user.id, - parent_id=current_parent_id, - is_folder=True - ) - db.session.add(new_folder) - db.session.flush() # To get the ID - current_parent_id = new_folder.id - - # Create file records for files in current directory - for filename in files: - full_path = os.path.join(root, filename) - secure_name = secure_filename(filename) - - # Generate UUID for storage - file_uuid = str(uuid.uuid4()) - storage_path = os.path.join(Config.UPLOAD_FOLDER, file_uuid) - - # Copy file to storage location - shutil.copy2(full_path, storage_path) + try: + # Save file to storage location + file.save(storage_path) # Get file info file_size = os.path.getsize(storage_path) @@ -150,499 +182,343 @@ def upload_folder(): # Create file record db_file = File( - name=secure_name, + name=filename, storage_name=file_uuid, mime_type=mime_type, size=file_size, user_id=current_user.id, - parent_id=current_parent_id, + parent_id=parent_folder.id if parent_folder else None, is_folder=False ) db.session.add(db_file) - - db.session.commit() - flash('Folder uploaded successfully', 'success') - except Exception as e: - db.session.rollback() - flash(f'Error uploading folder: {str(e)}', 'error') - finally: - # Clean up temp directory - shutil.rmtree(temp_dir, ignore_errors=True) - - # Redirect back to the folder - if parent_folder: - return redirect(url_for('files.browser', folder_id=parent_folder.id)) - else: - return redirect(url_for('files.browser')) - -@files_bp.route('/create_folder', methods=['GET', 'POST']) -@login_required -def create_folder(): - parent_id = request.args.get('folder', None, type=int) or request.form.get('parent_id', None, type=int) - - # Verify parent folder if specified - if parent_id: - parent = File.query.filter_by(id=parent_id, user_id=current_user.id, is_folder=True).first_or_404() - - if request.method == 'POST': - folder_name = request.form.get('folder_name', '').strip() - - if not folder_name: - flash('Folder name cannot be empty', 'error') - return redirect(request.url) - - # Secure the folder name - folder_name = secure_filename(folder_name) - - # Check if folder already exists - existing_folder = File.query.filter_by( - name=folder_name, - parent_id=parent_id, - user_id=current_user.id, - is_folder=True - ).first() - - if existing_folder: - flash(f'A folder named "{folder_name}" already exists', 'error') - return redirect(request.url) - - # Create new folder - new_folder = File( - name=folder_name, - user_id=current_user.id, - parent_id=parent_id, - is_folder=True - ) - - db.session.add(new_folder) - db.session.commit() - - flash(f'Folder "{folder_name}" created successfully', 'success') - - # Handle AJAX requests - if request.headers.get('X-Requested-With') == 'XMLHttpRequest': - return jsonify({'success': True, 'folder_id': new_folder.id}) - - # Redirect to the new folder - return redirect(url_for('files.browser', folder_id=new_folder.id)) - - # For GET, show the create folder form - return render_template('files/create_folder.html', - title='Create Folder', - parent_id=parent_id) - -@files_bp.route('/download/') -@login_required -def download(file_id): - file = File.query.filter_by(id=file_id, user_id=current_user.id, is_folder=False).first_or_404() - - # Record the download - download = Download( - file_id=file.id, - user_id=current_user.id, - ip_address=request.remote_addr - ) - db.session.add(download) - db.session.commit() - - return send_from_directory( - Config.UPLOAD_FOLDER, - file.storage_name, - as_attachment=True, - attachment_filename=file.name - ) - -@files_bp.route('/delete/', methods=['POST']) -@login_required -def delete(file_id): - file = File.query.filter_by(id=file_id, user_id=current_user.id).first_or_404() - parent_id = file.parent_id - - # If it's a folder, delete all children recursively - if file.is_folder: - delete_folder_recursive(file) - else: - # Delete the actual file - try: - os.remove(os.path.join(Config.UPLOAD_FOLDER, file.storage_name)) - except (FileNotFoundError, OSError): - pass # File already gone, continue - - # Delete the database record - db.session.delete(file) - - db.session.commit() - flash(f'{"Folder" if file.is_folder else "File"} deleted successfully', 'success') - - # Handle AJAX requests - if request.headers.get('X-Requested-With') == 'XMLHttpRequest': - return jsonify({'success': True}) - - return redirect(url_for('files.browser', folder_id=parent_id)) - -def delete_folder_recursive(folder): - """Recursively delete a folder and all its contents""" - # First get all child items - children = File.query.filter_by(parent_id=folder.id).all() - - for child in children: - if child.is_folder: - delete_folder_recursive(child) - else: - # Delete the actual file - try: - os.remove(os.path.join(Config.UPLOAD_FOLDER, child.storage_name)) - except (FileNotFoundError, OSError): - pass # File already gone, continue + db.session.commit() - # Delete the database record - db.session.delete(child) + # Return success response with file info + return jsonify({ + 'success': True, + 'file': { + 'id': db_file.id, + 'name': db_file.name, + 'size': db_file.size, + 'formatted_size': format_file_size(db_file.size), + 'mime_type': db_file.mime_type, + 'icon': get_file_icon(db_file.mime_type, db_file.name) + } + }) + + except Exception as e: + # Make sure to remove the file if there was an error + if os.path.exists(storage_path): + os.remove(storage_path) + + current_app.logger.error(f"Upload error: {str(e)}") + return jsonify({'error': str(e)}), 500 + + else: + # Regular form POST (non-XHR) - redirect to browser + flash('Please use the browser interface to upload files', 'info') + if parent_folder: + return redirect(url_for('files.browser', folder_id=parent_folder.id)) + else: + return redirect(url_for('files.browser')) - # Finally delete the folder itself - db.session.delete(folder) + # GET request - show upload page + return render_template('files/upload.html', + parent_folder=parent_folder, + title="Upload Files") -@files_bp.route('/rename/', methods=['POST']) +@files_bp.route('/upload_folder', methods=['POST']) @login_required -def rename(file_id): - file = File.query.filter_by(id=file_id, user_id=current_user.id).first_or_404() - new_name = request.form.get('new_name', '').strip() - - if not new_name: - flash('Name cannot be empty', 'error') - return redirect(url_for('files.browser', folder_id=file.parent_id)) - - # Secure the new name - new_name = secure_filename(new_name) - - # Check if a file/folder with this name already exists - existing = File.query.filter_by( - name=new_name, - parent_id=file.parent_id, - user_id=current_user.id, - is_folder=file.is_folder - ).first() - - if existing and existing.id != file.id: - flash(f'A {"folder" if file.is_folder else "file"} with this name already exists', 'error') - return redirect(url_for('files.browser', folder_id=file.parent_id)) - - # Update the name - file.name = new_name - db.session.commit() - - flash(f'{"Folder" if file.is_folder else "File"} renamed successfully', 'success') - - # Handle AJAX requests - if request.headers.get('X-Requested-With') == 'XMLHttpRequest': - return jsonify({'success': True}) - - return redirect(url_for('files.browser', folder_id=file.parent_id)) - -@files_bp.route('/share/', methods=['GET', 'POST']) -@login_required -def share(file_id): - file = File.query.filter_by(id=file_id, user_id=current_user.id).first_or_404() - - if request.method == 'POST': - # Generate share link - expires_days = request.form.get('expires', type=int) - - expires_at = None - if expires_days: - expires_at = datetime.utcnow() + timedelta(days=expires_days) - - # Create unique share token - share_token = str(uuid.uuid4()) - - # Save share in database - share = Share( - file_id=file.id, - user_id=current_user.id, - token=share_token, - expires_at=expires_at - ) - - db.session.add(share) - db.session.commit() - - # Generate the share URL - share_url = url_for('files.public_share', token=share_token, _external=True) - - flash('Share link created successfully', 'success') - return render_template('files/share_success.html', - title='Share Link', - file=file, - share=share, - share_url=share_url) - - return render_template('files/share.html', - title='Share File', - file=file) - -@files_bp.route('/public/') -def public_share(token): - # Find the share by token - share = Share.query.filter_by(token=token).first_or_404() - - # Check if share has expired - if share.expires_at and share.expires_at < datetime.utcnow(): - return render_template('files/share_expired.html', - title='Share Expired') - - # Get the file details - file = File.query.get_or_404(share.file_id) - - # Record the download - download = Download( - file_id=file.id, - share_id=share.id, - ip_address=request.remote_addr - ) - db.session.add(download) - db.session.commit() - - # If it's a viewable file type, show a preview - if file.mime_type and ( - file.mime_type.startswith('image/') or - file.mime_type == 'application/pdf' or - file.mime_type.startswith('text/') or - file.mime_type in ['application/javascript', 'application/json'] - ): - return render_template('files/preview.html', - title=file.name, - file=file, - share=share, - download_url=url_for('files.public_download', token=token)) - - # Otherwise, redirect to download - return redirect(url_for('files.public_download', token=token)) - -@files_bp.route('/public/download/') -def public_download(token): - # Find the share by token - share = Share.query.filter_by(token=token).first_or_404() - - # Check if share has expired - if share.expires_at and share.expires_at < datetime.utcnow(): - return render_template('files/share_expired.html', - title='Share Expired') - - # Get the file details - file = File.query.get_or_404(share.file_id) - - # Send the file - return send_from_directory( - Config.UPLOAD_FOLDER, - file.storage_name, - as_attachment=True, - attachment_filename=file.name - ) - -@files_bp.route('/upload_xhr', methods=['POST']) -@login_required -def upload_xhr(): - """Handle AJAX file uploads with progress tracking""" - if 'files[]' not in request.files: - return jsonify({'success': False, 'error': 'No files found in the request'}) - - files = request.files.getlist('files[]') +def upload_folder(): + """Handle folder upload - this processes ZIP files uploaded as folders""" folder_id = request.form.get('folder_id', None, type=int) - is_folder = request.form.get('is_folder') == '1' - paths = request.form.getlist('paths[]') - # Check if any files were selected - if not files or all(f.filename == '' for f in files): - return jsonify({'success': False, 'error': 'No files selected for upload'}) - - # Check folder exists if folder_id is provided - parent_folder = None - if folder_id: - parent_folder = File.query.filter_by(id=folder_id, user_id=current_user.id, is_folder=True).first() - if not parent_folder: - return jsonify({'success': False, 'error': 'Parent folder not found'}) - - # Process uploads - successful = 0 - failed = 0 - errors = [] - - # If this is a folder upload, we need to create the folder structure - folder_map = {} # Maps path to folder ID - - for i, file in enumerate(files): - try: - if file.filename == '': - continue - - # Get the relative path for folder uploads - relative_path = paths[i] if is_folder and i < len(paths) else None - - # Handle folder structure if needed - current_parent_id = folder_id - if is_folder and relative_path: - # Split path into directory components - path_parts = os.path.dirname(relative_path).split('/') - if path_parts and path_parts[0]: # Skip empty path (files at root) - # Create each folder in the path if needed - current_path = "" - for part in path_parts: - if not part: # Skip empty parts - continue - - current_path = os.path.join(current_path, part) if current_path else part - - # Check if we've already created this folder - if current_path in folder_map: - current_parent_id = folder_map[current_path] - continue - - # Check if folder already exists - folder_name = secure_filename(part) - existing_folder = File.query.filter_by( - name=folder_name, - parent_id=current_parent_id, - user_id=current_user.id, - is_folder=True - ).first() - - if existing_folder: - current_parent_id = existing_folder.id - folder_map[current_path] = existing_folder.id - else: - # Create new folder - new_folder = File( - name=folder_name, - parent_id=current_parent_id, - user_id=current_user.id, - is_folder=True - ) - db.session.add(new_folder) - db.session.flush() # Get the ID without committing - - current_parent_id = new_folder.id - folder_map[current_path] = new_folder.id - - # Now handle the actual file - filename = os.path.basename(relative_path) if relative_path else file.filename - filename = secure_filename(filename) - - # Check if file already exists - existing_file = File.query.filter_by( - name=filename, - parent_id=current_parent_id, - user_id=current_user.id, - is_folder=False - ).first() - - if existing_file: - # Create a unique name by adding timestamp - name_parts = os.path.splitext(filename) - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = f"{name_parts[0]}_{timestamp}{name_parts[1]}" - - # Generate a unique storage name - storage_name = f"{str(uuid.uuid4())}{os.path.splitext(filename)[1]}" - - # Save the file - file_path = os.path.join(Config.UPLOAD_FOLDER, storage_name) - file.save(file_path) - - # Get file size and mime type - file_size = os.path.getsize(file_path) - mime_type = mimetypes.guess_type(filename)[0] or 'application/octet-stream' - - # Create file entry in database - db_file = File( - name=filename, - storage_name=storage_name, - mime_type=mime_type, - size=file_size, - user_id=current_user.id, - parent_id=current_parent_id, - is_folder=False - ) - - db.session.add(db_file) - successful += 1 - - except Exception as e: - failed += 1 - errors.append(f"{file.filename}: {str(e)}") - - # Commit all database changes - if successful > 0: - db.session.commit() - - result = { - 'success': True if successful > 0 else False, - 'message': f"Successfully uploaded {successful} files, {failed} failed.", - 'successful': successful, - 'failed': failed, - 'errors': errors - } - - return jsonify(result) - -@files_bp.route('/contents') -@login_required -def folder_contents(): - """Returns the HTML for folder contents (used for AJAX loading)""" - folder_id = request.args.get('folder_id', None, type=int) - - if request.headers.get('X-Requested-With') != 'XMLHttpRequest': - # If not an AJAX request, redirect to browser view - return redirect(url_for('files.browser', folder_id=folder_id)) - - # Query parent folder parent_folder = None if folder_id: parent_folder = File.query.filter_by(id=folder_id, user_id=current_user.id, is_folder=True).first_or_404() - # Get files and subfolders - query = File.query.filter_by(user_id=current_user.id, parent_id=folder_id) - folders = query.filter_by(is_folder=True).order_by(File.name).all() - files = query.filter_by(is_folder=False).order_by(File.name).all() + # Check if folder data was provided + if 'folder_data' not in request.form: + return jsonify({'error': 'No folder data provided'}), 400 - return render_template('files/partials/folder_contents.html', - folders=folders, - files=files, - parent=parent_folder) + try: + folder_data = json.loads(request.form['folder_data']) + folder_name = secure_filename(folder_data.get('name', 'Unnamed Folder')) + + # Create folder record + folder = File( + name=folder_name, + is_folder=True, + user_id=current_user.id, + parent_id=parent_folder.id if parent_folder else None, + size=0, + mime_type=None + ) + db.session.add(folder) + db.session.flush() # Get folder.id without committing + + # Process files + file_records = [] + total_size = 0 + + # Create temp dir for uploaded files + temp_dir = os.path.join(Config.UPLOAD_FOLDER, 'temp', str(uuid.uuid4())) + os.makedirs(temp_dir, exist_ok=True) + + try: + for file_info in folder_data.get('files', []): + original_path = file_info.get('path', '') + + # Skip files without a valid path + if not original_path: + continue + + # Get relative path within the folder + path_parts = original_path.split('/') + + # Create intermediate folders as needed + current_parent_id = folder.id + + for i, part in enumerate(path_parts[:-1]): + part = secure_filename(part) + # Skip empty parts + if not part: + continue + + # Check if folder already exists + subfolder = File.query.filter_by( + name=part, + parent_id=current_parent_id, + user_id=current_user.id, + is_folder=True + ).first() + + if not subfolder: + # Create new subfolder + subfolder = File( + name=part, + is_folder=True, + user_id=current_user.id, + parent_id=current_parent_id, + size=0, + mime_type=None + ) + db.session.add(subfolder) + db.session.flush() + + current_parent_id = subfolder.id + + # Process the file if it exists in the request + file_uid = file_info.get('id') + if file_uid and file_uid in request.files: + file = request.files[file_uid] + filename = secure_filename(path_parts[-1]) + + # Generate UUID for storage + file_uuid = str(uuid.uuid4()) + storage_path = os.path.join(Config.UPLOAD_FOLDER, file_uuid) + + # Save file + file.save(storage_path) + + # Get file info + file_size = os.path.getsize(storage_path) + mime_type, _ = mimetypes.guess_type(filename) + if not mime_type: + mime_type = 'application/octet-stream' + + # Create file record + db_file = File( + name=filename, + storage_name=file_uuid, + mime_type=mime_type, + size=file_size, + user_id=current_user.id, + parent_id=current_parent_id, + is_folder=False + ) + db.session.add(db_file) + file_records.append(db_file) + total_size += file_size + + # Update folder size (sum of all files in the folder and subfolders) + folder.size = total_size + + # Commit all changes + db.session.commit() + + # Return success with folder info + return jsonify({ + 'success': True, + 'folder': { + 'id': folder.id, + 'name': folder.name + } + }) + + except Exception as e: + db.session.rollback() + current_app.logger.error(f"Folder upload error: {str(e)}") + return jsonify({'error': str(e)}), 500 + finally: + # Clean up temp directory + shutil.rmtree(temp_dir, ignore_errors=True) + + except Exception as e: + current_app.logger.error(f"Folder upload parsing error: {str(e)}") + return jsonify({'error': str(e)}), 500 -@files_bp.route('/preview/') +@files_bp.route('/download/') @login_required -def file_preview(file_id): - """Returns file preview data""" +def download(file_id): + """Download a file""" file = File.query.filter_by(id=file_id, user_id=current_user.id, is_folder=False).first_or_404() - result = { + # Can't download folders directly + if file.is_folder: + flash('Cannot download folders directly. Please use the ZIP option.', 'warning') + return redirect(url_for('files.browser', folder_id=file.id)) + + # Check if file exists in storage + storage_path = os.path.join(Config.UPLOAD_FOLDER, file.storage_name) + + if not os.path.exists(storage_path): + flash('File not found in storage', 'error') + return redirect(url_for('files.browser', folder_id=file.parent_id)) + + # Return the file + return send_file( + storage_path, + download_name=file.name, + as_attachment=True + ) + +@files_bp.route('/create_folder', methods=['POST']) +@login_required +def create_folder(): + """Create a new folder""" + parent_id = request.form.get('parent_id', type=int) + folder_name = request.form.get('name', '').strip() + + if not folder_name: + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({'error': 'Folder name is required'}), 400 + else: + flash('Folder name is required', 'error') + return redirect(url_for('files.browser', folder_id=parent_id)) + + # Sanitize folder name + folder_name = secure_filename(folder_name) + + # Check if folder already exists + parent = None + if parent_id: + parent = File.query.filter_by(id=parent_id, user_id=current_user.id, is_folder=True).first_or_404() + existing = File.query.filter_by( + name=folder_name, + parent_id=parent_id, + user_id=current_user.id, + is_folder=True + ).first() + else: + existing = File.query.filter_by( + name=folder_name, + parent_id=None, + user_id=current_user.id, + is_folder=True + ).first() + + if existing: + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({'error': 'A folder with this name already exists'}), 400 + else: + flash('A folder with this name already exists', 'error') + return redirect(url_for('files.browser', folder_id=parent_id)) + + # Create new folder + new_folder = File( + name=folder_name, + is_folder=True, + user_id=current_user.id, + parent_id=parent_id + ) + + db.session.add(new_folder) + db.session.commit() + + if request.headers.get('X-Requested-With') == 'XMLHttpRequest': + return jsonify({ + 'success': True, + 'folder': { + 'id': new_folder.id, + 'name': new_folder.name, + 'url': url_for('files.browser', folder_id=new_folder.id) + } + }) + else: + flash('Folder created successfully', 'success') + return redirect(url_for('files.browser', folder_id=parent_id)) + +@files_bp.route('/rename/', methods=['POST']) +@login_required +def rename(item_id): + """Rename a file or folder""" + item = File.query.filter_by(id=item_id, user_id=current_user.id).first_or_404() + new_name = request.form.get('name', '').strip() + + if not new_name: + return jsonify({'error': 'Name is required'}), 400 + + # Sanitize name + new_name = secure_filename(new_name) + + # Check if a file/folder with this name already exists in the same location + existing = File.query.filter( + File.name == new_name, + File.parent_id == item.parent_id, + File.user_id == current_user.id, + File.is_folder == item.is_folder, + File.id != item.id + ).first() + + if existing: + return jsonify({'error': 'An item with this name already exists'}), 400 + + # Update name + item.name = new_name + db.session.commit() + + return jsonify({ 'success': True, - 'file': { - 'id': file.id, - 'name': file.name, - 'mime_type': file.mime_type, - 'size': file.size, - 'created_at': file.created_at.isoformat() if file.created_at else None, - 'updated_at': file.updated_at.isoformat() if file.updated_at else None - }, - 'download_url': url_for('files.download', file_id=file.id), - 'preview_url': url_for('files.raw', file_id=file.id) - } - - return jsonify(result) + 'item': { + 'id': item.id, + 'name': item.name + } + }) -@files_bp.route('/raw/') +@files_bp.route('/delete/', methods=['POST']) @login_required -def raw(file_id): - """Serves raw file content for previews""" - file = File.query.filter_by(id=file_id, user_id=current_user.id, is_folder=False).first_or_404() +def delete(item_id): + """Delete a file or folder""" + item = File.query.filter_by(id=item_id, user_id=current_user.id).first_or_404() - # Check if file exists - file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], file.storage_name) - if not os.path.exists(file_path): - abort(404) + try: + # If it's a file, delete the actual file from storage + if not item.is_folder and item.storage_name: + storage_path = os.path.join(Config.UPLOAD_FOLDER, item.storage_name) + if os.path.exists(storage_path): + os.remove(storage_path) + + # Delete the database record (this will cascade delete any children due to the model relationship) + db.session.delete(item) + db.session.commit() + + return jsonify({'success': True}) - # Check file size for text files (only preview if under 2MB) - if file.mime_type and file.mime_type.startswith('text/') and file.size > 2 * 1024 * 1024: - return "File too large to preview", 413 - - return send_file(file_path, mimetype=file.mime_type) \ No newline at end of file + except Exception as e: + db.session.rollback() + current_app.logger.error(f"Delete error: {str(e)}") + return jsonify({'error': str(e)}), 500 + +# Import the helper functions from __init__.py +from app import get_file_icon, format_file_size \ No newline at end of file diff --git a/app/static/js/common.js b/app/static/js/common.js new file mode 100644 index 0000000..d7a4dee --- /dev/null +++ b/app/static/js/common.js @@ -0,0 +1,162 @@ +/** + * Common JavaScript functions used across the application + */ + +// Modal handling +function openModal(modalId) { + const modal = typeof modalId === 'string' ? document.getElementById(modalId) : modalId; + if (modal) { + modal.style.display = 'flex'; + document.body.classList.add('modal-open'); + } +} + +function closeModal(modalId) { + const modal = typeof modalId === 'string' ? document.getElementById(modalId) : modalId; + if (modal) { + modal.style.display = 'none'; + document.body.classList.remove('modal-open'); + } +} + +// Hide all modals on page load +function setupModals() { + document.querySelectorAll('.modal').forEach(modal => { + modal.style.display = 'none'; + }); + + // Close modals when clicking outside or on close button + document.addEventListener('click', function (e) { + if (e.target.classList.contains('modal')) { + closeModal(e.target); + } else if (e.target.classList.contains('modal-close') || e.target.classList.contains('modal-cancel')) { + const modal = e.target.closest('.modal'); + closeModal(modal); + } + }); + + // Escape key to close modals + document.addEventListener('keydown', function (e) { + if (e.key === 'Escape') { + document.querySelectorAll('.modal.visible').forEach(modal => { + closeModal(modal); + }); + } + }); +} + +// Alerts +function showAlert(message, type = 'info') { + // Create alerts container if it doesn't exist + let alertsContainer = document.querySelector('.alerts'); + if (!alertsContainer) { + alertsContainer = document.createElement('div'); + alertsContainer.className = 'alerts'; + document.body.appendChild(alertsContainer); + } + + // Create alert + const alert = document.createElement('div'); + alert.className = `alert ${type}`; + alert.innerHTML = ` +
${message}
+ + `; + + // Add to container + alertsContainer.appendChild(alert); + + // Setup dismiss + const closeBtn = alert.querySelector('.close'); + closeBtn.addEventListener('click', function () { + alert.classList.add('fade-out'); + setTimeout(() => { + alert.remove(); + }, 300); + }); + + // Auto dismiss + setTimeout(() => { + if (alert.parentNode) { + alert.classList.add('fade-out'); + setTimeout(() => { + if (alert.parentNode) { + alert.remove(); + } + }, 300); + } + }, 5000); +} + +// Helper functions +function formatSize(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; +} + +function formatDate(dateString) { + if (!dateString) return 'Unknown'; + const date = new Date(dateString); + return date.toLocaleString(); +} + +function escapeHtml(unsafe) { + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +function getFileIconClass(fileName) { + if (!fileName) return 'fa-file'; + + const ext = fileName.split('.').pop().toLowerCase(); + + // Images + if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg', 'webp'].includes(ext)) { + return 'fa-file-image'; + } + // Videos + else if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv'].includes(ext)) { + return 'fa-file-video'; + } + // Audio + else if (['mp3', 'wav', 'ogg', 'flac', 'm4a'].includes(ext)) { + return 'fa-file-audio'; + } + // Documents + else if (['doc', 'docx', 'odt'].includes(ext)) { + return 'fa-file-word'; + } + // Spreadsheets + else if (['xls', 'xlsx', 'ods', 'csv'].includes(ext)) { + return 'fa-file-excel'; + } + // Presentations + else if (['ppt', 'pptx', 'odp'].includes(ext)) { + return 'fa-file-powerpoint'; + } + // PDFs + else if (['pdf'].includes(ext)) { + return 'fa-file-pdf'; + } + // Archives + else if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2'].includes(ext)) { + return 'fa-file-archive'; + } + // Text + else if (['txt', 'rtf', 'md', 'log'].includes(ext)) { + return 'fa-file-alt'; + } + // Code + else if (['html', 'css', 'js', 'php', 'py', 'java', 'c', 'cpp', 'h', 'xml', 'json', 'sql'].includes(ext)) { + return 'fa-file-code'; + } + + return 'fa-file'; +} \ No newline at end of file diff --git a/app/templates/auth/profile.html b/app/templates/auth/profile.html index 66deea2..60e9a67 100644 --- a/app/templates/auth/profile.html +++ b/app/templates/auth/profile.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}Profile - Flask Files{% endblock %} +{% block title %}User Profile - Flask Files{% endblock %} {% block extra_css %} {% endblock %} {% block content %} -
-

User Profile

- -
- - - -
- - -
-
-
-
- {{ current_user.username[0].upper() }} -
-
+ {% endblock %} -{% block extra_js %} +{% block scripts %} diff --git a/app/templates/auth/settings.html b/app/templates/auth/settings.html new file mode 100644 index 0000000..82b38fd --- /dev/null +++ b/app/templates/auth/settings.html @@ -0,0 +1,436 @@ +{% extends "base.html" %} + +{% block title %}Account Settings - Flask Files{% endblock %} + +{% block content %} +
+
+
+

Account Settings

+ + Back to Profile + +
+ +
+
+

Appearance

+
+
Theme
+
+ + + +
+
+ +
+
+
File View
+
+
+ + +
+
+
+
+
+ +
+

Notifications

+
+
Email Notifications
+
+ +
+
+ Receive email notifications about file shares and new comments +
+
+
+ +
+

Privacy

+
+
Public Profile
+
+ +
+
+ Allow others to see your profile and shared files +
+
+ +
+
Share Statistics
+
+ +
+
+ Collect anonymous usage statistics to improve the service +
+
+
+ +
+

Danger Zone

+

+ These actions are permanent and cannot be undone +

+ +
+ + + +
+
+
+
+
+ + + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/app/templates/base.html b/app/templates/base.html index 4f367fe..815eba6 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,5 +1,5 @@ - + @@ -50,27 +50,38 @@ -
-