kinda working safe point

This commit is contained in:
pika 2025-03-23 03:29:05 +01:00
parent b9a82af12f
commit 6dda02141e
31 changed files with 4302 additions and 2937 deletions

View file

@ -0,0 +1,9 @@
---
description:
globs:
---
# Your rule content
- You can @ files here
- You can use markdown but dont have to

View file

@ -1,4 +1,4 @@
from flask import Flask, current_app
from flask import Flask, current_app, render_template
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from config import Config
@ -7,6 +7,13 @@ from datetime import datetime
import sqlite3
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Initialize extensions
db = SQLAlchemy()
login_manager = LoginManager()
@ -18,37 +25,57 @@ def initialize_database(app):
with app.app_context():
app.logger.info("Initializing database...")
try:
# Create all tables (this is safe to call even if tables exist)
db.create_all()
# Check if tables exist before creating them
from sqlalchemy import inspect
inspector = inspect(db.engine)
existing_tables = inspector.get_table_names()
# 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
# Only create tables that don't exist
if not existing_tables:
app.logger.info("Creating database tables...")
db.create_all()
# Check if we need to add the Share and Download models
inspector = db.inspect(db.engine)
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
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'):
# Create the Download table
Download.__table__.create(db.engine)
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")
# 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")
else:
app.logger.info(f"Database already initialized with tables: {existing_tables}")
# Check for missing tables
from app.models import User, File, Folder, Download
required_tables = ['users', 'files', 'folders', 'downloads']
missing_tables = [table for table in required_tables if table not in existing_tables]
if missing_tables:
app.logger.info(f"Creating missing tables: {missing_tables}")
# Create only the missing tables
db.create_all()
app.logger.info("Database initialization complete")
except Exception as e:
app.logger.error(f"Error initializing database: {str(e)}")
# Don't raise the exception to prevent app startup failure
@ -160,7 +187,17 @@ def format_file_size(size):
def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
# Configure app
if config_class:
app.config.from_object(config_class)
else:
# Use default configuration
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-key-change-in-production')
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL', 'sqlite:///app.db')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['UPLOAD_FOLDER'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), '../uploads')
app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # 100 MB max upload
# Configure logging
if not app.debug:
@ -198,13 +235,15 @@ def create_app(config_class=Config):
run_migrations(app)
# Register blueprints
from app.routes.auth import auth_bp
from app.routes.files import files_bp
from app.routes.dashboard import dashboard_bp
from app.routes.auth import bp as auth_bp
from app.routes.files import bp as files_bp
from app.routes.dashboard import bp as dashboard_bp
from app.routes.admin import bp as admin_bp
app.register_blueprint(auth_bp)
app.register_blueprint(files_bp)
app.register_blueprint(auth_bp, url_prefix='/auth')
app.register_blueprint(files_bp, url_prefix='/files')
app.register_blueprint(dashboard_bp)
app.register_blueprint(admin_bp, url_prefix='/admin')
# Add context processor for template variables
@app.context_processor
@ -227,8 +266,8 @@ def create_app(config_class=Config):
db.session.rollback() # Rollback any failed database transactions
return render_template('errors/500.html'), 500
logger.info("Flask Files startup")
return app
# Import must come after create_app to avoid circular imports
from flask import render_template # For error handlers
from app import models

View file

@ -0,0 +1,81 @@
"""
Automatic database migrations system
"""
import logging
from sqlalchemy import inspect
from .. import db
logger = logging.getLogger(__name__)
class Migration:
"""Base migration class"""
# Higher version numbers run later
version = 0
description = "Base migration"
def should_run(self, inspector):
"""Determine if this migration should run"""
return True
def run(self):
"""Execute the migration"""
raise NotImplementedError
class AddFolderIdToFiles(Migration):
"""Add folder_id column to files table"""
version = 1
description = "Add folder_id column to files table"
def should_run(self, inspector):
"""Check if folder_id column exists in files table"""
if 'files' not in inspector.get_table_names():
return False
columns = [col['name'] for col in inspector.get_columns('files')]
return 'folder_id' not in columns
def run(self):
"""Add the folder_id column and foreign key constraint"""
try:
db.engine.execute('ALTER TABLE files ADD COLUMN folder_id INTEGER;')
db.engine.execute('ALTER TABLE files ADD CONSTRAINT fk_files_folder_id FOREIGN KEY (folder_id) REFERENCES folders (id);')
logger.info("Added folder_id column to files table")
return True
except Exception as e:
logger.error(f"Error adding folder_id column: {str(e)}")
return False
# Add all migrations here
MIGRATIONS = [
AddFolderIdToFiles(),
# Remove the Share table migration since we're not using it
]
def run_migrations():
"""Run all pending migrations"""
logger.info("Checking for pending database migrations...")
inspector = inspect(db.engine)
# Sort migrations by version
pending_migrations = sorted([m for m in MIGRATIONS if m.should_run(inspector)],
key=lambda m: m.version)
if not pending_migrations:
logger.info("No pending migrations found.")
return
logger.info(f"Found {len(pending_migrations)} pending migrations.")
success_count = 0
for migration in pending_migrations:
logger.info(f"Running migration {migration.version}: {migration.description}")
try:
success = migration.run()
if success:
success_count += 1
else:
logger.warning(f"Migration {migration.version} reported failure")
except Exception as e:
logger.error(f"Error in migration {migration.version}: {str(e)}")
logger.info(f"Migration complete. {success_count}/{len(pending_migrations)} migrations successful.")

View file

@ -0,0 +1,29 @@
"""
Migration script to add folder_id column to files table
"""
from flask import Flask
from app import create_app, db
from app.models import File, Folder
def run_migration():
"""Add folder_id column to files table if it doesn't exist"""
app = create_app()
with app.app_context():
# Check if the column exists
from sqlalchemy import inspect
inspector = inspect(db.engine)
columns = [col['name'] for col in inspector.get_columns('files')]
if 'folder_id' not in columns:
print("Adding folder_id column to files table...")
# Add the column
db.engine.execute('ALTER TABLE files ADD COLUMN folder_id INTEGER;')
# Add foreign key constraint
db.engine.execute('ALTER TABLE files ADD CONSTRAINT fk_files_folder_id FOREIGN KEY (folder_id) REFERENCES folders (id);')
print("Column added successfully!")
else:
print("folder_id column already exists")
if __name__ == "__main__":
run_migration()

View file

@ -0,0 +1,36 @@
"""
Add Share table for file and folder sharing
"""
from sqlalchemy import inspect
from .. import db
class AddShareTable:
"""Migration to add the shares table"""
version = 2
description = "Add Share table for file/folder sharing"
def should_run(self, inspector):
"""Check if the shares table exists"""
return 'shares' not in inspector.get_table_names()
def run(self):
"""Create the shares table"""
try:
db.engine.execute('''
CREATE TABLE shares (
id INTEGER PRIMARY KEY,
item_type VARCHAR(10) NOT NULL,
item_id INTEGER NOT NULL,
owner_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
permission VARCHAR(10) NOT NULL DEFAULT 'view',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (owner_id) REFERENCES users (id),
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
return True
except Exception as e:
print(f"Error creating shares table: {str(e)}")
return False

View file

@ -1,109 +1,181 @@
from datetime import datetime
import logging
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
from app import db, login_manager
import uuid
import os
import mimetypes
# Add debug logging
logger = logging.getLogger(__name__)
logger.info("Loading models.py module")
@login_manager.user_loader
def load_user(id):
return User.query.get(int(id))
class User(UserMixin, db.Model):
__tablename__ = 'user'
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
username = db.Column(db.String(64), unique=True, index=True)
email = db.Column(db.String(120), unique=True, index=True)
password_hash = db.Column(db.String(128))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
last_login = db.Column(db.DateTime)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
is_admin = db.Column(db.Boolean, default=False)
# 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')
# Define relationships without circular backrefs
folders = db.relationship('Folder', foreign_keys='Folder.user_id',
backref=db.backref('owner', lazy='joined'),
lazy='dynamic', cascade="all, delete-orphan")
files = db.relationship('File', foreign_keys='File.user_id',
backref=db.backref('owner', lazy='joined'),
lazy='dynamic', cascade="all, delete-orphan")
downloads = db.relationship('Download', foreign_keys='Download.user_id',
backref=db.backref('downloader', lazy='joined'),
lazy='dynamic', cascade="all, delete-orphan")
# Add shares relationship for consistency with existing database
shares_created = db.relationship('Share', foreign_keys='Share.user_id',
backref=db.backref('creator', lazy='joined'),
lazy='dynamic', cascade="all, delete-orphan")
@property
def password(self):
raise AttributeError('password is not a readable attribute')
@password.setter
def password(self, password):
self.password_hash = generate_password_hash(password)
logger.info(f"Password set for user {self.username}")
# Add set_password method for compatibility with existing code
def set_password(self, password):
"""Set user password - compatibility method"""
logger.info(f"set_password called for {self.username}")
self.password_hash = generate_password_hash(password)
def verify_password(self, password):
return check_password_hash(self.password_hash, password)
def __repr__(self):
return f'<User {self.username}>'
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
class File(db.Model):
__tablename__ = 'file'
# Log that the User class is defined
logger.info("User class defined with set_password method")
class Folder(db.Model):
__tablename__ = 'folders'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False)
storage_name = db.Column(db.String(255)) # Used for storing files with unique names
is_folder = db.Column(db.Boolean, default=False)
mime_type = db.Column(db.String(128))
size = db.Column(db.Integer, default=0) # Size in bytes
parent_id = db.Column(db.Integer, db.ForeignKey('file.id', ondelete='CASCADE'), nullable=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
parent_id = db.Column(db.Integer, db.ForeignKey('folders.id'))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# 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')
# Define relationship to parent folder with proper backref
parent = db.relationship('Folder', remote_side=[id],
backref=db.backref('children', lazy='dynamic'))
# Define relationship to files in this folder
files = db.relationship('File', foreign_keys='File.folder_id',
backref=db.backref('folder', lazy='joined'),
lazy='dynamic', cascade="all, delete-orphan")
def get_path(self):
"""Get the full path of the folder"""
if self.parent is None:
return "/" + self.name
return self.parent.get_path() + "/" + self.name
def __repr__(self):
return f'<File {self.name} {"(Folder)" if self.is_folder else ""}>'
return f'<Folder {self.name}>'
class File(db.Model):
__tablename__ = 'files'
def generate_storage_name(self):
"""Generate a unique name for file storage to prevent conflicts"""
if self.is_folder:
return None
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False)
original_name = db.Column(db.String(255), nullable=False)
path = db.Column(db.String(255), nullable=False)
size = db.Column(db.Integer, nullable=False)
type = db.Column(db.String(128))
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
folder_id = db.Column(db.Integer, db.ForeignKey('folders.id'))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
# Define relationships
shares = db.relationship('Share', backref='file', lazy='dynamic')
@property
def icon_class(self):
"""Get the appropriate Font Awesome icon class based on file extension"""
extension = os.path.splitext(self.name)[1].lower()
# Generate a unique filename using UUID
ext = self.name.rsplit('.', 1)[1].lower() if '.' in self.name else ''
return f"{uuid.uuid4().hex}.{ext}" if ext else f"{uuid.uuid4().hex}"
# Map extensions to icon classes
icon_map = {
'.pdf': 'fa-file-pdf',
'.doc': 'fa-file-word', '.docx': 'fa-file-word',
'.xls': 'fa-file-excel', '.xlsx': 'fa-file-excel',
'.ppt': 'fa-file-powerpoint', '.pptx': 'fa-file-powerpoint',
'.jpg': 'fa-file-image', '.jpeg': 'fa-file-image', '.png': 'fa-file-image',
'.gif': 'fa-file-image', '.svg': 'fa-file-image',
'.mp3': 'fa-file-audio', '.wav': 'fa-file-audio', '.ogg': 'fa-file-audio',
'.mp4': 'fa-file-video', '.avi': 'fa-file-video', '.mov': 'fa-file-video',
'.zip': 'fa-file-archive', '.rar': 'fa-file-archive', '.tar': 'fa-file-archive',
'.gz': 'fa-file-archive',
'.txt': 'fa-file-alt', '.md': 'fa-file-alt',
'.html': 'fa-file-code', '.css': 'fa-file-code', '.js': 'fa-file-code',
'.py': 'fa-file-code', '.java': 'fa-file-code', '.c': 'fa-file-code'
}
return icon_map.get(extension, 'fa-file')
def get_mime_type(self):
"""Get the MIME type based on the file extension"""
mime_type, _ = mimetypes.guess_type(self.name)
return mime_type or 'application/octet-stream'
def __repr__(self):
return f'<File {self.name}>'
class Share(db.Model):
__tablename__ = 'share'
__tablename__ = 'shares'
id = db.Column(db.Integer, primary_key=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)
file_id = db.Column(db.Integer, db.ForeignKey('files.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
access_key = db.Column(db.String(64), unique=True, index=True, nullable=False)
expires_at = db.Column(db.DateTime, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
download_count = db.Column(db.Integer, default=0)
def is_expired(self):
"""Check if the share has expired"""
if self.expires_at is None:
return False
return datetime.utcnow() > self.expires_at
def __repr__(self):
return f'<Share {self.share_token}>'
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
return f'<Share {self.access_key}>'
class Download(db.Model):
__tablename__ = 'download'
__tablename__ = 'downloads'
id = db.Column(db.Integer, primary_key=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=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)
file_id = db.Column(db.Integer, db.ForeignKey('files.id'), nullable=False)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
download_date = db.Column(db.DateTime, default=datetime.utcnow)
# Define relationship to file
file = db.relationship('File', foreign_keys=[file_id],
backref=db.backref('file_downloads', lazy='dynamic'))
def __repr__(self):
return f'<Download of File {self.file_id} at {self.downloaded_at}>'
return f'<Download {self.id} by {self.user_id}>'

47
app/routes/admin.py Normal file
View file

@ -0,0 +1,47 @@
from flask import Blueprint, render_template, jsonify, redirect, url_for, flash, current_app
from flask_login import login_required, current_user
from ..models import User
from ..migrations import run_migrations
from ..utils.reset_db import reset_database
bp = Blueprint('admin', __name__, url_prefix='/admin')
@bp.route('/')
@login_required
def index():
"""Admin panel home"""
if not current_user.is_admin:
flash('Access denied. Admin privileges required.', 'error')
return redirect(url_for('dashboard.index'))
return render_template('admin/panel.html')
@bp.route('/run-migrations', methods=['POST'])
@login_required
def trigger_migrations():
"""Manually trigger database migrations (admin only)"""
# Check if user is admin
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized. Admin privileges required'}), 403
# Run migrations
with current_app.app_context():
run_migrations()
return jsonify({'success': True, 'message': 'Migrations completed successfully'})
@bp.route('/reset-database', methods=['POST'])
@login_required
def reset_db():
"""Reset the entire database (admin only)"""
# Check if user is admin
if not current_user.is_admin:
return jsonify({'error': 'Unauthorized. Admin privileges required'}), 403
# Reset database
success = reset_database()
if success:
return jsonify({'success': True, 'message': 'Database reset successfully. You will be logged out.'})
else:
return jsonify({'error': 'Failed to reset database. Check logs for details.'}), 500

View file

@ -1,4 +1,4 @@
from flask import render_template, redirect, url_for, flash, request, current_app, jsonify, session
from flask import Blueprint, render_template, redirect, url_for, flash, request, current_app, jsonify, session
from flask_login import login_user, logout_user, login_required, current_user
from urllib.parse import urlparse
from app import db
@ -8,6 +8,13 @@ from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired, Length, EqualTo, ValidationError
from werkzeug.exceptions import BadRequest
from werkzeug.security import generate_password_hash
import logging
# Setup logging
logger = logging.getLogger(__name__)
bp = Blueprint('auth', __name__)
# Login form
class LoginForm(FlaskForm):
@ -28,51 +35,77 @@ class RegistrationForm(FlaskForm):
if user is not None:
raise ValidationError('Please use a different username.')
@auth_bp.route('/login', methods=['GET', 'POST'])
@bp.route('/login', methods=['GET', 'POST'])
def login():
"""User login page"""
if current_user.is_authenticated:
return redirect(url_for('dashboard.index'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.username.data).first()
if user is None or not user.check_password(form.password.data):
if user is None or not user.verify_password(form.password.data):
flash('Invalid username or password', 'error')
return redirect(url_for('auth.login'))
login_user(user, remember=form.remember_me.data)
# Redirect to requested page or dashboard
next_page = request.args.get('next')
if not next_page or urlparse(next_page).netloc != '':
next_page = url_for('dashboard.index')
flash('Login successful!', 'success')
return redirect(next_page)
return render_template('auth/login.html', title='Sign In', form=form)
return render_template('auth/login.html', form=form)
@auth_bp.route('/logout')
@bp.route('/logout')
@login_required
def logout():
"""User logout"""
logout_user()
flash('You have been logged out', 'info')
flash('You have been logged out.', 'info')
return redirect(url_for('auth.login'))
@auth_bp.route('/register', methods=['GET', 'POST'])
@bp.route('/register', methods=['GET', 'POST'])
def register():
"""User registration page"""
if current_user.is_authenticated:
return redirect(url_for('dashboard.index'))
form = RegistrationForm()
if form.validate_on_submit():
user = User(username=form.username.data)
user.set_password(form.password.data)
# Create new user
user = User(
username=form.username.data,
)
# Try both ways to set password
try:
# First try with set_password method
logger.info("Trying to set password with set_password method")
if hasattr(user, 'set_password'):
user.set_password(form.password.data)
else:
# Fall back to property setter
logger.info("set_password not found, using password property instead")
user.password = form.password.data
except Exception as e:
logger.error(f"Error setting password: {e}")
# Ensure we set the password somehow
user.password_hash = generate_password_hash(form.password.data)
logger.info("Set password_hash directly")
# Save to database
db.session.add(user)
db.session.commit()
logger.info(f"User {user.username} registered successfully")
flash('Registration successful! You can now log in.', 'success')
return redirect(url_for('auth.login'))
return render_template('auth/register.html', title='Register', form=form)
return render_template('auth/register.html', form=form)
@auth_bp.route('/update_profile', methods=['POST'])
@login_required
@ -136,12 +169,8 @@ def update_preferences():
flash('Preferences updated successfully', 'success')
return redirect(url_for('auth.profile'))
@auth_bp.route('/profile')
@bp.route('/profile')
@login_required
def profile():
# Get theme preference from session or default to system
theme_preference = session.get('theme_preference', 'system')
return render_template('auth/profile.html',
title='User Profile',
theme_preference=theme_preference)
"""User profile page"""
return render_template('auth/profile.html', user=current_user)

View file

@ -1,59 +1,46 @@
from flask import Blueprint, render_template
from flask import Blueprint, render_template, redirect, url_for
from flask_login import login_required, current_user
from datetime import datetime, timedelta
from app.models import File, Share, Download
from ..models import File, Folder
import os
dashboard_bp = Blueprint('dashboard', __name__)
# Create blueprint with the name expected by __init__.py
bp = Blueprint('dashboard', __name__)
@dashboard_bp.route('/')
@bp.route('/')
@bp.route('/index')
@login_required
def index():
# Get some stats for the dashboard
total_files = File.query.filter_by(user_id=current_user.id, is_folder=False).count()
total_folders = File.query.filter_by(user_id=current_user.id, is_folder=True).count()
"""Dashboard index page"""
# Count user's files and folders
file_count = File.query.filter_by(user_id=current_user.id).count()
folder_count = Folder.query.filter_by(user_id=current_user.id).count()
# Recent files for quick access
recent_files = File.query.filter_by(user_id=current_user.id, is_folder=False)\
.order_by(File.updated_at.desc())\
.limit(8).all()
# Get storage usage
storage_used = sum(file.size for file in File.query.filter_by(user_id=current_user.id).all())
# Root folders for quick navigation
root_folders = File.query.filter_by(user_id=current_user.id, is_folder=True, parent_id=None)\
.order_by(File.name)\
.limit(8).all()
# Format size for display
if storage_used < 1024:
storage_used_formatted = f"{storage_used} bytes"
elif storage_used < 1024 * 1024:
storage_used_formatted = f"{storage_used / 1024:.2f} KB"
elif storage_used < 1024 * 1024 * 1024:
storage_used_formatted = f"{storage_used / (1024 * 1024):.2f} MB"
else:
storage_used_formatted = f"{storage_used / (1024 * 1024 * 1024):.2f} GB"
# Count active shares (if Share model exists)
active_shares = 0
recent_activities = 0
# Get recent files
recent_files = File.query.filter_by(user_id=current_user.id).order_by(File.created_at.desc()).limit(5).all()
# Check if Share and Download models exist/are imported
try:
# Count active shares
active_shares = Share.query.filter_by(user_id=current_user.id).filter(
(Share.expires_at > datetime.now()) | (Share.expires_at.is_(None))
).count()
# Recent activities count (downloads, shares, etc.)
recent_activities = Download.query.join(Share)\
.filter(Share.user_id == current_user.id)\
.filter(Download.timestamp > (datetime.now() - timedelta(days=7)))\
.count()
except:
# Models not ready yet, using default values
pass
# Create stats object that the template is expecting
stats = {
'file_count': file_count,
'folder_count': folder_count,
'storage_used': storage_used_formatted
}
return render_template('dashboard.html',
title='Dashboard',
total_files=total_files,
total_folders=total_folders,
recent_files=recent_files,
root_folders=root_folders,
active_shares=active_shares,
recent_activities=recent_activities,
now=datetime.now(),
file_icon=get_file_icon,
format_size=format_file_size)
return render_template('dashboard/index.html',
stats=stats, # Pass as stats object
recent_files=recent_files)
def get_file_icon(mime_type, filename):
"""Return Font Awesome icon class based on file type"""

View file

@ -2,7 +2,7 @@ from flask import Blueprint, render_template, redirect, url_for, flash, request,
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.models import File, Share, Folder
from config import Config
import os
from datetime import datetime, timedelta
@ -10,12 +10,13 @@ import uuid
import mimetypes
import shutil
import json
import time
files_bp = Blueprint('files', __name__, url_prefix='/files')
bp = Blueprint('files', __name__, url_prefix='/files')
@files_bp.route('/')
@files_bp.route('/browser')
@files_bp.route('/browser/<int:folder_id>')
@bp.route('/')
@bp.route('/browser')
@bp.route('/browser/<int:folder_id>')
@login_required
def browser(folder_id=None):
"""Display file browser interface"""
@ -23,7 +24,7 @@ def browser(folder_id=None):
breadcrumbs = []
if folder_id:
current_folder = File.query.filter_by(id=folder_id, user_id=current_user.id, is_folder=True).first_or_404()
current_folder = Folder.query.filter_by(id=folder_id, user_id=current_user.id).first_or_404()
# Generate breadcrumbs
breadcrumbs = []
@ -36,22 +37,30 @@ def browser(folder_id=None):
# 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()
folders = Folder.query.filter_by(parent_id=current_folder.id, user_id=current_user.id).all()
files = File.query.filter_by(folder_id=current_folder.id, user_id=current_user.id).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()
folders = Folder.query.filter_by(parent_id=None, user_id=current_user.id).all()
files = File.query.filter_by(folder_id=None, user_id=current_user.id).all()
return render_template('files/browser.html',
# Get the search query if provided
query = request.args.get('q', '')
if query:
# Filter folders and files by name containing the query
folders = [f for f in folders if query.lower() in f.name.lower()]
files = [f for f in files if query.lower() in f.name.lower()]
return render_template('files/browser.html',
current_folder=current_folder,
breadcrumbs=breadcrumbs,
folders=folders,
folders=folders,
files=files)
else:
# If it's an AJAX request, return JSON
return jsonify({'error': 'Use the /contents endpoint for AJAX requests'})
# For AJAX request, return just the folder contents
# Implement this if needed
pass
@files_bp.route('/contents')
@bp.route('/contents')
@login_required
def folder_contents():
"""Returns the HTML for folder contents (used for AJAX loading)"""
@ -63,31 +72,31 @@ def folder_contents():
# 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()
current_folder = Folder.query.filter_by(id=folder_id, user_id=current_user.id).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)
folders_query = Folder.query.filter_by(user_id=current_user.id)
files_query = File.query.filter_by(user_id=current_user.id)
# 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)
files_query = files_query.filter_by(folder_id=current_folder.id)
else:
folders_query = folders_query.filter_by(parent_id=None)
files_query = files_query.filter_by(parent_id=None)
files_query = files_query.filter_by(folder_id=None)
# Apply search if provided
if search_query:
folders_query = folders_query.filter(File.name.ilike(f'%{search_query}%'))
folders_query = folders_query.filter(Folder.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())
folders_query = folders_query.order_by(Folder.name.asc() if sort_order == 'asc' else Folder.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())
folders_query = folders_query.order_by(Folder.updated_at.desc() if sort_order == 'asc' else Folder.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
@ -107,7 +116,7 @@ def folder_contents():
'name': folder.name,
'updated_at': folder.updated_at.isoformat(),
'is_folder': True,
'item_count': folder.children.count(),
'item_count': folder.files.count(),
'url': url_for('files.browser', folder_id=folder.id)
} for folder in folders]
@ -116,10 +125,10 @@ def folder_contents():
'name': file.name,
'size': file.size,
'formatted_size': format_file_size(file.size),
'mime_type': file.mime_type,
'type': file.type,
'updated_at': file.updated_at.isoformat(),
'is_folder': False,
'icon': get_file_icon(file.mime_type, file.name),
'icon': file.icon_class,
'url': url_for('files.download', file_id=file.id)
} for file in files]
@ -141,34 +150,53 @@ def folder_contents():
else:
return redirect(url_for('files.browser'))
@files_bp.route('/upload', methods=['GET', 'POST'])
@files_bp.route('/upload/<int:folder_id>', methods=['GET', 'POST'])
@bp.route('/upload', methods=['GET', 'POST'])
@bp.route('/upload/<int:folder_id>', 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:
# Handle file upload
if 'file' not in request.files:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'error': 'No file part'}), 400
file = request.files['file']
# Check if the file was actually selected
if file.filename == '':
flash('No file part', 'error')
return redirect(request.url)
file = request.files['file']
# Validate filename
if file.filename == '':
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'error': 'No selected file'}), 400
# Validate and save file
flash('No selected file', 'error')
return redirect(request.url)
# Get the parent folder
parent_folder = None
if folder_id:
parent_folder = Folder.query.get_or_404(folder_id)
# Check if user has permission
if parent_folder.user_id != current_user.id:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'error': 'You do not have permission to upload to this folder'}), 403
flash('You do not have permission to upload to this folder', 'error')
return redirect(url_for('files.browser'))
# Process the file
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
# Generate UUID for storage
# Generate a unique filename
file_uuid = str(uuid.uuid4())
storage_path = os.path.join(Config.UPLOAD_FOLDER, file_uuid)
_, file_extension = os.path.splitext(filename)
storage_name = f"{file_uuid}{file_extension}"
# Create storage path
upload_folder = current_app.config['UPLOAD_FOLDER']
user_folder = os.path.join(upload_folder, str(current_user.id))
os.makedirs(user_folder, exist_ok=True)
storage_path = os.path.join(user_folder, storage_name)
try:
# Save file to storage location
@ -183,12 +211,12 @@ def upload(folder_id=None):
# Create file record
db_file = File(
name=filename,
storage_name=file_uuid,
mime_type=mime_type,
original_name=filename,
path=storage_path,
size=file_size,
type=mime_type,
user_id=current_user.id,
parent_id=parent_folder.id if parent_folder else None,
is_folder=False
folder_id=parent_folder.id if parent_folder else None
)
db.session.add(db_file)
db.session.commit()
@ -201,8 +229,8 @@ def upload(folder_id=None):
'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)
'type': db_file.type,
'icon': db_file.icon_class
}
})
@ -215,19 +243,15 @@ def upload(folder_id=None):
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'))
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return jsonify({'error': 'File type not allowed'}), 400
flash('File type not allowed', 'error')
return redirect(request.url)
# GET request - show upload page
return render_template('files/upload.html',
parent_folder=parent_folder,
title="Upload Files")
# GET request - show upload form
return render_template('files/upload.html', folder_id=folder_id)
@files_bp.route('/upload_folder', methods=['POST'])
@bp.route('/upload_folder', methods=['POST'])
@login_required
def upload_folder():
"""Handle folder upload - this processes ZIP files uploaded as folders"""
@ -235,7 +259,7 @@ def upload_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()
parent_folder = Folder.query.filter_by(id=folder_id, user_id=current_user.id).first_or_404()
# Check if folder data was provided
if 'folder_data' not in request.form:
@ -246,13 +270,10 @@ def upload_folder():
folder_name = secure_filename(folder_data.get('name', 'Unnamed Folder'))
# Create folder record
folder = File(
folder = Folder(
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
parent_id=parent_folder.id if parent_folder else None
)
db.session.add(folder)
db.session.flush() # Get folder.id without committing
@ -286,22 +307,18 @@ def upload_folder():
continue
# Check if folder already exists
subfolder = File.query.filter_by(
subfolder = Folder.query.filter_by(
name=part,
parent_id=current_parent_id,
user_id=current_user.id,
is_folder=True
user_id=current_user.id
).first()
if not subfolder:
# Create new subfolder
subfolder = File(
subfolder = Folder(
name=part,
is_folder=True,
user_id=current_user.id,
parent_id=current_parent_id,
size=0,
mime_type=None
parent_id=current_parent_id
)
db.session.add(subfolder)
db.session.flush()
@ -330,12 +347,12 @@ def upload_folder():
# Create file record
db_file = File(
name=filename,
storage_name=file_uuid,
mime_type=mime_type,
original_name=filename,
path=storage_path,
size=file_size,
type=mime_type,
user_id=current_user.id,
parent_id=current_parent_id,
is_folder=False
folder_id=current_parent_id
)
db.session.add(db_file)
file_records.append(db_file)
@ -368,23 +385,23 @@ def upload_folder():
current_app.logger.error(f"Folder upload parsing error: {str(e)}")
return jsonify({'error': str(e)}), 500
@files_bp.route('/download/<int:file_id>')
@bp.route('/download/<int:file_id>')
@login_required
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()
file = File.query.filter_by(id=file_id, user_id=current_user.id).first_or_404()
# Can't download folders directly
if file.is_folder:
if file.folder:
flash('Cannot download folders directly. Please use the ZIP option.', 'warning')
return redirect(url_for('files.browser', folder_id=file.id))
return redirect(url_for('files.browser', folder_id=file.folder.id))
# Check if file exists in storage
storage_path = os.path.join(Config.UPLOAD_FOLDER, file.storage_name)
storage_path = file.path
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 redirect(url_for('files.browser', folder_id=file.folder_id))
# Return the file
return send_file(
@ -393,131 +410,336 @@ def download(file_id):
as_attachment=True
)
@files_bp.route('/create_folder', methods=['POST'])
@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()
name = request.form.get('name')
parent_id = request.form.get('parent_id')
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))
if not name:
flash('Folder name is required', 'danger')
return redirect(url_for('files.browser'))
# 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,
# Create folder
folder = Folder(
name=name,
user_id=current_user.id,
parent_id=parent_id
parent_id=parent_id if parent_id else None
)
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/<int:item_id>', 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,
'item': {
'id': item.id,
'name': item.name
}
})
@files_bp.route('/delete/<int:item_id>', methods=['POST'])
@login_required
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()
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.add(folder)
db.session.commit()
return jsonify({'success': True})
flash(f'Folder "{name}" created successfully', 'success')
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Delete error: {str(e)}")
current_app.logger.error(f"Error creating folder: {str(e)}")
flash('Error creating folder', 'danger')
# Redirect to appropriate location
if parent_id:
return redirect(url_for('files.browser', folder_id=parent_id))
return redirect(url_for('files.browser'))
@bp.route('/rename', methods=['POST'])
@login_required
def rename_item():
"""Rename a file or folder"""
try:
# Get JSON data
data = request.get_json()
if not data:
return jsonify({'error': 'No data provided'}), 400
item_id = data.get('item_id')
new_name = data.get('new_name')
# Validation
if not item_id or not new_name or new_name.strip() == '':
return jsonify({'error': 'Item ID and new name are required'}), 400
# Determine if it's a file or folder
file = File.query.filter_by(id=item_id, user_id=current_user.id).first()
folder = Folder.query.filter_by(id=item_id, user_id=current_user.id).first()
if file:
# For files, we need to handle the file system and database
old_path = file.path
file_dir = os.path.dirname(old_path)
# Create safe name
safe_name = secure_filename(new_name)
# Check for duplicates
existing_file = File.query.filter_by(
name=safe_name,
folder_id=file.folder_id,
user_id=current_user.id
).filter(File.id != file.id).first()
if existing_file:
return jsonify({'error': 'A file with this name already exists'}), 400
# Update file path
new_path = os.path.join(file_dir, safe_name)
# Rename file on disk
try:
if os.path.exists(old_path):
os.rename(old_path, new_path)
except OSError as e:
return jsonify({'error': f'Error renaming file: {str(e)}'}), 500
# Update database
file.name = safe_name
file.path = new_path
db.session.commit()
return jsonify({
'success': True,
'message': 'File renamed successfully',
'new_name': safe_name
})
elif folder:
# For folders, we just update the database
# Check for duplicates
existing_folder = Folder.query.filter_by(
name=new_name,
parent_id=folder.parent_id,
user_id=current_user.id
).filter(Folder.id != folder.id).first()
if existing_folder:
return jsonify({'error': 'A folder with this name already exists'}), 400
# Update folder name
folder.name = new_name
db.session.commit()
return jsonify({
'success': True,
'message': 'Folder renamed successfully',
'new_name': new_name
})
else:
return jsonify({'error': 'Item not found'}), 404
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error renaming item: {str(e)}")
return jsonify({'error': str(e)}), 500
@bp.route('/delete/<item_id>', methods=['POST'])
@login_required
def delete_item(item_id):
"""Delete a file or folder"""
try:
# Check if item exists
if not item_id or item_id == 'null':
return jsonify({'error': 'Invalid item ID'}), 400
# Determine if it's a file or folder
file = File.query.filter_by(id=item_id, user_id=current_user.id).first()
folder = Folder.query.filter_by(id=item_id, user_id=current_user.id).first()
if file:
# Delete file from storage
try:
if os.path.exists(file.path):
os.remove(file.path)
except OSError as e:
current_app.logger.error(f"Error deleting file from disk: {str(e)}")
# Delete from database
db.session.delete(file)
db.session.commit()
return jsonify({'success': True, 'message': 'File deleted successfully'})
elif folder:
# Check if folder has contents
has_files = File.query.filter_by(folder_id=folder.id).first() is not None
has_subfolders = Folder.query.filter_by(parent_id=folder.id).first() is not None
if has_files or has_subfolders:
# Delete recursively
delete_folder_recursive(folder.id)
return jsonify({'success': True, 'message': 'Folder and contents deleted successfully'})
else:
# Empty folder, simple delete
db.session.delete(folder)
db.session.commit()
return jsonify({'success': True, 'message': 'Folder deleted successfully'})
else:
return jsonify({'error': 'Item not found'}), 404
except Exception as e:
db.session.rollback()
current_app.logger.error(f"Error deleting item: {str(e)}")
return jsonify({'error': str(e)}), 500
def delete_folder_recursive(folder_id):
"""Recursively delete a folder and its contents"""
# Delete all files in the folder
files = File.query.filter_by(folder_id=folder_id).all()
for file in files:
try:
if os.path.exists(file.path):
os.remove(file.path)
except OSError:
pass
db.session.delete(file)
# Recursively delete subfolders
subfolders = Folder.query.filter_by(parent_id=folder_id).all()
for subfolder in subfolders:
delete_folder_recursive(subfolder.id)
# Delete the folder itself
folder = Folder.query.get(folder_id)
if folder:
db.session.delete(folder)
@bp.route('/upload_xhr', methods=['POST'])
@login_required
def upload_xhr():
"""Handle XHR file uploads with improved error handling"""
try:
# Get parent folder ID if provided
parent_folder_id = request.form.get('parent_folder_id')
parent_folder = None
if parent_folder_id:
parent_folder = Folder.query.get(parent_folder_id)
if not parent_folder or parent_folder.user_id != current_user.id:
return jsonify({'error': 'Invalid parent folder'}), 400
# Check if files were uploaded
if 'files[]' not in request.files:
return jsonify({'error': 'No files in request'}), 400
files = request.files.getlist('files[]')
if not files or len(files) == 0 or files[0].filename == '':
return jsonify({'error': 'No files selected'}), 400
# Process files
uploaded_files = []
for file in files:
# Handle folder uploads by parsing the path
path_parts = []
if '/' in file.filename:
# This is a file in a folder structure
path_parts = file.filename.split('/')
filename = path_parts[-1] # Last part is the actual filename
# Create folder structure
current_parent = parent_folder
for i, folder_name in enumerate(path_parts[:-1]):
if not folder_name: # Skip empty folder names
continue
# Check if folder already exists
existing_folder = Folder.query.filter_by(
name=folder_name,
parent_id=current_parent.id if current_parent else None,
user_id=current_user.id
).first()
if existing_folder:
current_parent = existing_folder
else:
# Create new folder
new_folder = Folder(
name=folder_name,
parent_id=current_parent.id if current_parent else None,
user_id=current_user.id
)
db.session.add(new_folder)
db.session.flush() # Get ID without committing
current_parent = new_folder
else:
# Regular file upload
filename = file.filename
current_parent = parent_folder
# Save the file
if not filename:
continue # Skip files with empty names
secure_name = secure_filename(filename)
# Check for duplicates
existing_file = File.query.filter_by(
name=secure_name,
folder_id=current_parent.id if current_parent else None,
user_id=current_user.id
).first()
if existing_file:
# Append timestamp to avoid overwrite
name_parts = secure_name.rsplit('.', 1)
if len(name_parts) > 1:
secure_name = f"{name_parts[0]}_{int(time.time())}.{name_parts[1]}"
else:
secure_name = f"{secure_name}_{int(time.time())}"
# Create file path
file_dir = os.path.join(current_app.config['UPLOAD_FOLDER'], str(current_user.id))
# Ensure directory exists
os.makedirs(file_dir, exist_ok=True)
file_path = os.path.join(file_dir, secure_name)
# Save file to disk
file.save(file_path)
# Create file record in database
file_size = os.path.getsize(file_path)
file_type = file.content_type or 'application/octet-stream'
new_file = File(
name=secure_name,
original_name=filename,
path=file_path,
size=file_size,
type=file_type,
folder_id=current_parent.id if current_parent else None,
user_id=current_user.id
)
db.session.add(new_file)
uploaded_files.append({
'id': None, # Will be set after commit
'name': secure_name,
'size': file_size,
'type': file_type
})
# Commit all changes
db.session.commit()
# Update file IDs for response
for i, file_data in enumerate(uploaded_files):
if i < len(db.session.new):
file_data['id'] = db.session.new[i].id
return jsonify({
'success': True,
'message': f'Successfully uploaded {len(uploaded_files)} files',
'files': uploaded_files
})
except Exception as e:
# Log the error for debugging
current_app.logger.error(f"Upload error: {str(e)}")
db.session.rollback()
return jsonify({'error': str(e)}), 500
# Import the helper functions from __init__.py

View file

@ -1,3 +1,408 @@
.browser-container {
background: var(--card-bg);
border-radius: var(--border-radius);
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: var(--shadow-sm);
}
.browser-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.browser-title {
display: flex;
align-items: center;
gap: 0.5rem;
}
.browser-actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
.breadcrumbs {
display: flex;
flex-wrap: wrap;
margin-bottom: 1.5rem;
background: var(--bg-light);
padding: 0.5rem 1rem;
border-radius: var(--border-radius-sm);
}
.breadcrumb-item {
display: flex;
align-items: center;
}
.breadcrumb-separator {
margin: 0 0.5rem;
color: var(--text-muted);
}
.view-toggle {
display: flex;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
overflow: hidden;
margin-left: 0.5rem;
}
.view-btn {
border: none;
background: var(--card-bg);
padding: 0.5rem;
cursor: pointer;
color: var(--text-muted);
}
.view-btn.active {
background: var(--primary-color);
color: white;
}
.filter-bar {
display: flex;
margin-bottom: 1rem;
gap: 0.5rem;
flex-wrap: wrap;
}
.search-bar {
flex-grow: 1;
position: relative;
}
.search-bar input {
width: 100%;
padding: 0.5rem 1rem 0.5rem 2.5rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
background: var(--bg-light);
}
.search-icon {
position: absolute;
left: 0.75rem;
top: 0.75rem;
color: var(--text-muted);
pointer-events: none;
}
.sort-dropdown {
position: relative;
}
.sort-dropdown-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
background: var(--bg-light);
cursor: pointer;
}
.sort-dropdown-menu {
position: absolute;
top: 100%;
right: 0;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
min-width: 200px;
z-index: 10;
box-shadow: var(--shadow-md);
display: none;
margin-top: 0.25rem;
}
.sort-dropdown-menu.show {
display: block;
}
.sort-option {
padding: 0.5rem 1rem;
cursor: pointer;
}
.sort-option:hover {
background: var(--bg-hover);
}
.sort-option.active {
color: var(--primary-color);
font-weight: 500;
}
.files-container {
min-height: 200px;
position: relative;
}
.loading-indicator {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(var(--card-bg-rgb), 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.loading-indicator.show {
opacity: 1;
pointer-events: auto;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(var(--primary-color-rgb), 0.3);
border-radius: 50%;
border-top-color: var(--primary-color);
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Grid view */
.files-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 1rem;
}
.folder-item,
.file-item {
display: flex;
flex-direction: column;
padding: 1rem;
border-radius: var(--border-radius-sm);
border: 1px solid var(--border-color);
transition: all 0.2s ease;
position: relative;
text-decoration: none;
color: var(--text-color);
}
.folder-item:hover,
.file-item:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-sm);
background: var(--bg-hover);
}
.item-icon {
font-size: 3rem;
margin-bottom: 0.75rem;
text-align: center;
color: var(--primary-color);
}
.folder-item .item-icon {
color: #ffc107;
}
.item-info {
flex: 1;
display: flex;
flex-direction: column;
}
.item-name {
font-weight: 500;
margin-bottom: 0.25rem;
word-break: break-word;
text-align: center;
}
.item-details {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
color: var(--text-muted);
text-align: center;
}
/* List view */
.files-list {
display: flex;
flex-direction: column;
}
.list-view .folder-item,
.list-view .file-item {
flex-direction: row;
align-items: center;
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
}
.list-view .item-icon {
font-size: 1.5rem;
margin-bottom: 0;
margin-right: 1rem;
width: 1.5rem;
text-align: center;
}
.list-view .item-info {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.list-view .item-name {
margin-bottom: 0;
text-align: left;
}
.list-view .item-details {
margin-left: auto;
min-width: 200px;
justify-content: flex-end;
}
.list-view .item-date {
margin-left: 1rem;
}
/* Empty folder state */
.empty-folder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 1rem;
text-align: center;
}
.empty-icon {
font-size: 3rem;
color: var(--text-muted);
margin-bottom: 1rem;
}
.empty-message h3 {
margin-bottom: 0.5rem;
font-weight: 500;
}
.empty-message p {
color: var(--text-muted);
margin-bottom: 1.5rem;
}
.empty-actions {
display: flex;
gap: 0.5rem;
}
/* Context menu */
.context-menu {
position: fixed;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
box-shadow: var(--shadow-md);
z-index: 100;
min-width: 180px;
padding: 0.5rem 0;
}
.context-menu-item {
padding: 0.5rem 1rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
}
.context-menu-item:hover {
background: var(--bg-hover);
}
.context-menu-item.danger {
color: var(--danger-color);
}
/* Responsive */
@media (max-width: 768px) {
.browser-header {
flex-direction: column;
align-items: flex-start;
}
.browser-actions {
width: 100%;
justify-content: space-between;
}
.filter-bar {
flex-direction: column;
}
.files-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
.list-view .item-info {
flex-direction: column;
align-items: flex-start;
}
.list-view .item-details {
margin-left: 0;
margin-top: 0.25rem;
}
}
/* Modal fixes - ensure they're hidden by default */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
overflow: auto;
justify-content: center;
align-items: center;
}
.modal.visible {
display: flex;
}
.modal-content {
background: var(--card-bg);
border-radius: var(--border-radius);
width: 100%;
max-width: 500px;
box-shadow: var(--shadow-lg);
margin: 2rem;
}
/* File Browser Styles */
.browser-container {
background: var(--card-bg);

View file

@ -0,0 +1,52 @@
/* Context Menu Styles */
.context-menu {
position: absolute;
display: none;
background-color: var(--card-bg);
border-radius: 6px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
padding: 8px 0;
min-width: 180px;
z-index: 1000;
animation: context-menu-appear 0.2s ease;
border: 1px solid var(--border-color);
}
@keyframes context-menu-appear {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.context-menu-item {
padding: 10px 16px;
cursor: pointer;
transition: background-color 0.2s;
font-size: 14px;
display: flex;
align-items: center;
color: var(--text-color);
}
.context-menu-item:hover {
background-color: var(--primary-color);
color: white;
}
.context-menu-item i {
margin-right: 10px;
width: 16px;
text-align: center;
}
.context-menu-divider {
height: 1px;
background-color: var(--border-color);
margin: 5px 0;
}

File diff suppressed because it is too large Load diff

130
app/static/css/modal.css Normal file
View file

@ -0,0 +1,130 @@
/* Beautiful modal styles */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(3px);
z-index: 1000;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.modal.visible {
opacity: 1;
}
.modal-content {
background-color: var(--card-bg);
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
width: 90%;
max-width: 400px;
position: relative;
transform: scale(0.95);
opacity: 0;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.modal.visible .modal-content {
transform: scale(1);
opacity: 1;
}
.modal-header {
padding: 1.25rem 1.5rem;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-weight: 600;
color: var(--text);
font-size: 1.25rem;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-secondary);
transition: color 0.2s ease;
line-height: 1;
padding: 0;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.modal-close:hover {
color: var(--danger-color);
background-color: rgba(var(--danger-color-rgb), 0.1);
}
.modal-body {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text);
}
.form-group input[type="text"],
.form-group input[type="password"],
.form-group input[type="email"] {
width: 100%;
padding: 0.75rem 1rem;
border-radius: 8px;
border: 1px solid var(--border-color);
background-color: var(--bg);
color: var(--text);
font-size: 1rem;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.form-group input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(var(--primary-color-rgb), 0.2);
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 1.5rem;
}
.form-actions .btn {
padding: 0.6rem 1.25rem;
border-radius: 8px;
font-weight: 500;
transition: background 0.3s ease, transform 0.2s ease;
}
.form-actions .btn:hover {
transform: translateY(-1px);
}
.form-actions .btn:active {
transform: translateY(0);
}

609
app/static/css/styles.css Normal file
View file

@ -0,0 +1,609 @@
/* Add these styles to your existing styles.css file */
/* Theme Toggle */
.theme-toggle-icon {
cursor: pointer;
padding: 0.5rem;
border-radius: 50%;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
}
.theme-toggle-icon:hover {
background-color: rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] .theme-toggle-icon:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* Make sure theme transitions work */
body,
.card,
.navbar,
.sidebar,
input,
select,
textarea,
button,
.modal-content,
.file-item,
.folder-item {
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
}
/* Making sure the styles apply properly to light/dark themes */
:root {
--primary-color: #4a6bff;
--primary-hover: #3a5bed;
--secondary-color: #6c757d;
--success-color: #28a745;
--danger-color: #dc3545;
--warning-color: #ffc107;
--info-color: #17a2b8;
--light-color: #f8f9fa;
--dark-color: #343a40;
--background-color: #1e2029;
--card-bg: #282a36;
--text-color: #f8f8f2;
--text-muted: #bd93f9;
--border-color: #44475a;
--transition-speed: 0.3s;
}
[data-theme="dark"] {
/* Ensure these are applied */
--bg: #121418;
--card-bg: #1e2029;
--text: #f2f3f8;
--primary-color-rgb: 109, 93, 252;
}
/* Theme script fix */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
transition: background-color 0.3s ease;
}
/* Global dropzone overlay for quick uploads */
.global-dropzone {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(5px);
z-index: 9999;
display: none;
justify-content: center;
align-items: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.global-dropzone.active {
display: flex;
opacity: 1;
}
.dropzone-content {
text-align: center;
color: white;
padding: 2rem;
border-radius: 12px;
background-color: rgba(var(--primary-color-rgb), 0.3);
border: 3px dashed rgba(255, 255, 255, 0.5);
max-width: 500px;
width: 90%;
animation: pulse 2s infinite;
}
.dropzone-icon {
margin-bottom: 1.5rem;
}
.dropzone-content h3 {
font-size: 1.8rem;
margin-bottom: 0.75rem;
}
.dropzone-content p {
opacity: 0.8;
font-size: 1.1rem;
}
/* Upload toast notification */
.upload-toast {
position: fixed;
bottom: 2rem;
right: 2rem;
width: 350px;
background-color: var(--card-bg);
border-radius: 10px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
z-index: 1080;
overflow: hidden;
display: none;
transform: translateY(20px);
opacity: 0;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.upload-toast.active {
display: block;
transform: translateY(0);
opacity: 1;
}
.upload-toast-header {
display: flex;
align-items: center;
padding: 0.8rem 1rem;
background-color: var(--primary-color);
color: white;
}
.upload-toast-header i {
margin-right: 0.75rem;
}
.upload-toast-close {
margin-left: auto;
background: none;
border: none;
color: white;
font-size: 1.25rem;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease;
}
.upload-toast-close:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.upload-toast-body {
padding: 1rem;
}
.upload-toast-progress-info {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.upload-toast-progress-bar-container {
height: 8px;
background-color: var(--border-color);
border-radius: 4px;
overflow: hidden;
}
.upload-toast-progress-bar {
height: 100%;
width: 0;
background-color: var(--primary-color);
transition: width 0.3s ease;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.03);
}
100% {
transform: scale(1);
}
}
/* File and folder views */
.files-container {
padding: 20px;
min-height: 300px;
}
/* Grid view */
.files-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.grid-view .folder-item,
.grid-view .file-item {
padding: 15px;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
background-color: rgba(0, 0, 0, 0.2);
border: 1px solid var(--border-color);
position: relative;
transition: all 0.3s;
text-decoration: none;
color: var(--text-color);
height: 150px;
}
.grid-view .item-icon {
font-size: 2.5rem;
margin-bottom: 10px;
color: var(--primary-color);
}
.grid-view .folder-item .item-icon {
color: #f1c40f;
}
.grid-view .item-info {
width: 100%;
}
.grid-view .item-name {
font-weight: 500;
margin-bottom: 5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.grid-view .item-details {
font-size: 0.8rem;
color: var(--text-muted);
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 8px;
}
/* File actions */
.file-actions {
position: absolute;
top: 5px;
right: 5px;
display: flex;
gap: 5px;
opacity: 0;
transition: opacity 0.2s;
}
.folder-item:hover .file-actions,
.file-item:hover .file-actions {
opacity: 1;
}
.action-btn {
background: rgba(0, 0, 0, 0.5);
border: none;
color: white;
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.action-btn:hover {
background: var(--primary-color);
}
.action-btn.edit:hover {
background: var(--info-color);
}
.action-btn.delete:hover {
background: var(--danger-color);
}
/* Empty folder */
.empty-folder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
text-align: center;
}
.empty-icon {
font-size: 3rem;
color: var(--border-color);
margin-bottom: 15px;
}
.empty-message h3 {
margin-bottom: 10px;
font-weight: 500;
}
.empty-message p {
color: var(--text-muted);
margin-bottom: 20px;
}
.empty-actions {
display: flex;
gap: 10px;
}
/* Main styles for the file management system */
.browser-container {
background-color: var(--card-bg);
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
margin-bottom: 30px;
overflow: hidden;
}
.browser-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid var(--border-color);
}
.browser-title {
display: flex;
align-items: center;
}
.browser-title h2 {
margin: 0 0 0 10px;
font-size: 1.5rem;
}
.browser-title i {
font-size: 1.5rem;
color: var(--primary-color);
}
.browser-actions {
display: flex;
align-items: center;
gap: 10px;
}
/* Breadcrumbs */
.breadcrumbs {
display: flex;
align-items: center;
padding: 10px 20px;
border-bottom: 1px solid var(--border-color);
overflow-x: auto;
white-space: nowrap;
}
.breadcrumb-item {
color: var(--text-muted);
text-decoration: none;
font-size: 0.9rem;
padding: 5px 0;
}
.breadcrumb-item:hover {
color: var(--primary-color);
}
.breadcrumb-separator {
margin: 0 8px;
color: var(--secondary-color);
}
/* Search bar */
.search-container {
position: relative;
margin-right: 10px;
}
.search-container input {
padding: 8px 15px 8px 35px;
border-radius: 20px;
border: 1px solid var(--border-color);
background-color: rgba(0, 0, 0, 0.2);
color: var(--text-color);
width: 200px;
transition: all 0.3s;
}
.search-container input:focus {
width: 250px;
outline: none;
border-color: var(--primary-color);
}
.search-btn {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
}
/* View toggle */
.view-toggle {
display: flex;
border-radius: 4px;
overflow: hidden;
margin-right: 10px;
}
.view-btn {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
color: var(--text-muted);
padding: 8px 12px;
cursor: pointer;
transition: all 0.2s;
}
.view-btn:first-child {
border-radius: 4px 0 0 4px;
}
.view-btn:last-child {
border-radius: 0 4px 4px 0;
}
.view-btn.active {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
/* Buttons */
.btn {
padding: 8px 15px;
border-radius: 4px;
border: none;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 8px;
}
.btn.primary {
background-color: var(--primary-color);
color: white;
}
.btn.primary:hover {
background-color: var(--primary-hover);
}
.btn.secondary {
background-color: transparent;
color: var(--text-color);
border: 1px solid var(--border-color);
}
.btn.secondary:hover {
background-color: rgba(255, 255, 255, 0.1);
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background-color: var(--card-bg);
border-radius: 8px;
width: 100%;
max-width: 500px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.2);
animation: modal-appear 0.3s forwards;
}
@keyframes modal-appear {
from {
opacity: 0;
transform: translateY(-50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid var(--border-color);
}
.modal-header h3 {
margin: 0;
font-size: 1.25rem;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-color);
opacity: 0.7;
transition: opacity 0.2s;
}
.modal-close:hover {
opacity: 1;
}
.modal-body {
padding: 20px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 10px;
border-radius: 4px;
border: 1px solid var(--border-color);
background-color: rgba(0, 0, 0, 0.2);
color: var(--text-color);
}
.form-group input:focus {
outline: none;
border-color: var(--primary-color);
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}

246
app/static/js/browser.js Normal file
View file

@ -0,0 +1,246 @@
/**
* File browser functionality
*/
document.addEventListener('DOMContentLoaded', function () {
// View toggle functionality
const filesContainer = document.getElementById('files-container');
const gridViewBtn = document.getElementById('grid-view-btn');
const listViewBtn = document.getElementById('list-view-btn');
if (filesContainer && gridViewBtn && listViewBtn) {
// Set initial view based on saved preference
const savedView = localStorage.getItem('view_preference') || 'grid';
filesContainer.className = `files-container ${savedView}-view`;
// Highlight the correct button
if (savedView === 'grid') {
gridViewBtn.classList.add('active');
listViewBtn.classList.remove('active');
} else {
listViewBtn.classList.add('active');
gridViewBtn.classList.remove('active');
}
// Add event listeners
gridViewBtn.addEventListener('click', function () {
filesContainer.className = 'files-container grid-view';
gridViewBtn.classList.add('active');
listViewBtn.classList.remove('active');
// Save preference
localStorage.setItem('view_preference', 'grid');
});
listViewBtn.addEventListener('click', function () {
filesContainer.className = 'files-container list-view';
listViewBtn.classList.add('active');
gridViewBtn.classList.remove('active');
// Save preference
localStorage.setItem('view_preference', 'list');
});
}
// Variables for tracking selected items
let selectedItemId = null;
let selectedItemType = null;
// Setup context menu functionality
function setupContextMenu() {
// Context menu already implemented via context-menu.js
// We'll just need to ensure our item actions are properly set
// Add item click handler to set selected item
document.querySelectorAll('.file-item, .folder-item').forEach(item => {
item.addEventListener('click', function (e) {
// If clicking on an action button, don't select the item
if (e.target.closest('.item-actions') || e.target.closest('a')) {
return;
}
// Set selected item
selectedItemId = this.dataset.id;
selectedItemType = this.classList.contains('folder-item') ? 'folder' : 'file';
// Highlight selected item
document.querySelectorAll('.file-item, .folder-item').forEach(i => {
i.classList.remove('selected');
});
this.classList.add('selected');
});
// Right-click to open context menu
item.addEventListener('contextmenu', function (e) {
// Set selected item
selectedItemId = this.dataset.id;
selectedItemType = this.classList.contains('folder-item') ? 'folder' : 'file';
// Highlight selected item
document.querySelectorAll('.file-item, .folder-item').forEach(i => {
i.classList.remove('selected');
});
this.classList.add('selected');
});
});
}
// Handle folder creation
const newFolderForm = document.getElementById('new-folder-form');
if (newFolderForm) {
newFolderForm.addEventListener('submit', function (e) {
e.preventDefault();
const folderName = document.getElementById('folder-name').value;
const parentId = document.querySelector('input[name="parent_id"]').value;
// Create FormData
const formData = new FormData();
formData.append('name', folderName);
if (parentId) formData.append('parent_id', parentId);
// Send request
fetch('/files/create_folder', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Close modal
closeModal('new-folder-modal');
// Show success message
showAlert('Folder created successfully', 'success');
// Reload page to show new folder
setTimeout(() => window.location.reload(), 500);
} else {
showAlert(data.error || 'Error creating folder', 'error');
}
})
.catch(error => {
showAlert('Error creating folder: ' + error, 'error');
});
});
}
// Handle rename
const renameForm = document.getElementById('rename-form');
if (renameForm) {
renameForm.addEventListener('submit', function (e) {
e.preventDefault();
const newName = document.getElementById('new-name').value;
if (!selectedItemId) {
showAlert('No item selected', 'error');
return;
}
fetch('/files/rename', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
item_id: selectedItemId,
new_name: newName
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Close modal
closeModal('rename-modal');
// Show success message
showAlert('Item renamed successfully', 'success');
// Update item name in the UI or reload
setTimeout(() => window.location.reload(), 500);
} else {
showAlert(data.error || 'Failed to rename item', 'error');
}
})
.catch(error => {
showAlert('Error: ' + error, 'error');
});
});
}
// Handle delete confirmation
const confirmDeleteBtn = document.getElementById('confirm-delete-btn');
if (confirmDeleteBtn) {
confirmDeleteBtn.addEventListener('click', function () {
if (!selectedItemId) {
showAlert('No item selected', 'error');
closeModal('delete-modal');
return;
}
fetch(`/files/delete/${selectedItemId}`, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Close modal
closeModal('delete-modal');
// Show success message
showAlert(data.message || 'Item deleted successfully', 'success');
// Remove the item from the UI or reload
setTimeout(() => window.location.reload(), 500);
} else {
showAlert(data.error || 'Failed to delete item', 'error');
}
})
.catch(error => {
showAlert('Error: ' + error, 'error');
});
});
}
// Initialize
setupContextMenu();
// Buttons to open modals
const deleteBtn = document.getElementById('delete-btn');
if (deleteBtn) {
deleteBtn.addEventListener('click', function () {
if (!selectedItemId) {
showAlert('Please select an item first', 'warning');
return;
}
openModal('delete-modal');
});
}
const renameBtn = document.getElementById('rename-btn');
if (renameBtn) {
renameBtn.addEventListener('click', function () {
if (!selectedItemId) {
showAlert('Please select an item first', 'warning');
return;
}
// Get current name
const selectedItem = document.querySelector(`.file-item[data-id="${selectedItemId}"], .folder-item[data-id="${selectedItemId}"]`);
const currentName = selectedItem ? selectedItem.querySelector('.item-name').textContent.trim() : '';
// Set current name in the input
document.getElementById('new-name').value = currentName;
openModal('rename-modal');
document.getElementById('new-name').focus();
});
}
});

View file

@ -0,0 +1,144 @@
/**
* Context Menu for Files/Folders
*/
class ContextMenu {
constructor() {
this.menu = null;
this.currentTarget = null;
this.init();
}
init() {
// Create context menu element
this.menu = document.createElement('div');
this.menu.className = 'context-menu';
this.menu.style.display = 'none';
document.body.appendChild(this.menu);
// Close menu on click outside
document.addEventListener('click', (e) => {
if (this.menu.style.display === 'block') {
this.hideMenu();
}
});
// Prevent default context menu
document.addEventListener('contextmenu', (e) => {
if (e.target.closest('.file-item, .folder-item')) {
e.preventDefault();
this.showMenu(e);
}
});
// Close on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.menu.style.display === 'block') {
this.hideMenu();
}
});
}
showMenu(e) {
// Get target item
this.currentTarget = e.target.closest('.file-item, .folder-item');
const itemId = this.currentTarget.dataset.id;
const itemType = this.currentTarget.classList.contains('folder-item') ? 'folder' : 'file';
const itemName = this.currentTarget.querySelector('.item-name').textContent;
// Create menu items based on item type
this.menu.innerHTML = '';
if (itemType === 'folder') {
// Folder actions
this.addMenuItem('Open', 'fa-folder-open', () => {
window.location.href = `/files/browse/${itemId}`;
});
this.addMenuItem('Rename', 'fa-edit', () => {
openModal('rename-modal');
document.getElementById('new-name').value = itemName;
window.selectedItemId = itemId;
});
this.addMenuItem('Delete', 'fa-trash-alt', () => {
openModal('delete-modal');
window.selectedItemId = itemId;
});
} else {
// File actions
this.addMenuItem('Download', 'fa-download', () => {
window.location.href = `/files/download/${itemId}`;
});
this.addMenuItem('View', 'fa-eye', () => {
window.location.href = `/files/view/${itemId}`;
});
this.addMenuItem('Rename', 'fa-edit', () => {
openModal('rename-modal');
document.getElementById('new-name').value = itemName;
window.selectedItemId = itemId;
});
this.addMenuItem('Delete', 'fa-trash-alt', () => {
openModal('delete-modal');
window.selectedItemId = itemId;
});
}
// Position menu
const x = e.clientX;
const y = e.clientY;
// Set menu position
this.menu.style.left = `${x}px`;
this.menu.style.top = `${y}px`;
// Show menu with animation
this.menu.style.display = 'block';
// Adjust position if menu goes off screen
const menuRect = this.menu.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
if (menuRect.right > windowWidth) {
this.menu.style.left = `${windowWidth - menuRect.width - 10}px`;
}
if (menuRect.bottom > windowHeight) {
this.menu.style.top = `${windowHeight - menuRect.height - 10}px`;
}
}
hideMenu() {
this.menu.style.display = 'none';
this.currentTarget = null;
}
addMenuItem(label, icon, action) {
const item = document.createElement('div');
item.className = 'context-menu-item';
item.innerHTML = `<i class="fas ${icon}"></i> ${label}`;
item.addEventListener('click', (e) => {
e.stopPropagation();
this.hideMenu();
action();
});
this.menu.appendChild(item);
}
addDivider() {
const divider = document.createElement('div');
divider.className = 'context-menu-divider';
this.menu.appendChild(divider);
}
}
// Initialize context menu
document.addEventListener('DOMContentLoaded', function () {
window.contextMenu = new ContextMenu();
});

208
app/static/js/main.js Normal file
View file

@ -0,0 +1,208 @@
// Main JavaScript file for Flask Files
document.addEventListener('DOMContentLoaded', function () {
// Initialize components
initializeViewToggle();
initializeFolderNavigation();
initializeModals();
initializeContextMenu();
initializeUploadFunctionality();
// Register service worker if supported
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/static/js/service-worker.js')
.then(function (registration) {
console.log('Service Worker registered with scope:', registration.scope);
}).catch(function (error) {
console.log('Service Worker registration failed:', error);
});
}
});
// Toggle between grid and list views
function initializeViewToggle() {
const gridViewBtn = document.getElementById('grid-view-btn');
const listViewBtn = document.getElementById('list-view-btn');
const filesContainer = document.getElementById('files-container');
if (gridViewBtn && listViewBtn && filesContainer) {
gridViewBtn.addEventListener('click', function () {
filesContainer.classList.add('grid-view');
filesContainer.classList.remove('list-view');
gridViewBtn.classList.add('active');
listViewBtn.classList.remove('active');
localStorage.setItem('fileViewPreference', 'grid');
});
listViewBtn.addEventListener('click', function () {
filesContainer.classList.add('list-view');
filesContainer.classList.remove('grid-view');
listViewBtn.classList.add('active');
gridViewBtn.classList.remove('active');
localStorage.setItem('fileViewPreference', 'list');
});
// Load user preference from localStorage
const viewPreference = localStorage.getItem('fileViewPreference') || 'grid';
if (viewPreference === 'grid') {
gridViewBtn.click();
} else {
listViewBtn.click();
}
}
}
// Add animations for folder navigation
function initializeFolderNavigation() {
// Add click event to folder items
document.querySelectorAll('.folder-item').forEach(folder => {
folder.addEventListener('click', function (e) {
e.preventDefault();
const href = this.getAttribute('href');
const filesContainer = document.getElementById('files-container');
// Add transition class
filesContainer.classList.add('changing');
// After a short delay, navigate to the folder
setTimeout(() => {
window.location.href = href;
}, 200);
});
});
// Add the animation class when page loads
const filesContainer = document.getElementById('files-container');
if (filesContainer) {
// Remove the class to trigger animation
filesContainer.classList.add('folder-enter-active');
}
}
// Modal handling
function initializeModals() {
// New folder modal
const newFolderBtn = document.getElementById('new-folder-btn');
const newFolderModal = document.getElementById('new-folder-modal');
const emptyNewFolderBtn = document.getElementById('empty-new-folder-btn');
if (newFolderBtn && newFolderModal) {
newFolderBtn.addEventListener('click', function () {
newFolderModal.style.display = 'flex';
document.getElementById('folder-name').focus();
});
if (emptyNewFolderBtn) {
emptyNewFolderBtn.addEventListener('click', function () {
newFolderModal.style.display = 'flex';
document.getElementById('folder-name').focus();
});
}
// Close modal
document.querySelectorAll('.modal-close, .modal-cancel').forEach(btn => {
btn.addEventListener('click', function () {
newFolderModal.style.display = 'none';
});
});
// Close on click outside
window.addEventListener('click', function (event) {
if (event.target === newFolderModal) {
newFolderModal.style.display = 'none';
}
});
}
}
// Context menu for right-click on files/folders
function initializeContextMenu() {
const contextMenu = document.getElementById('context-menu');
if (!contextMenu) return;
document.addEventListener('contextmenu', function (e) {
const fileItem = e.target.closest('.file-item, .folder-item');
if (fileItem) {
e.preventDefault();
const itemId = fileItem.getAttribute('data-id');
const itemType = fileItem.classList.contains('file-item') ? 'file' : 'folder';
// Position menu
contextMenu.style.left = `${e.pageX}px`;
contextMenu.style.top = `${e.pageY}px`;
// Show menu
contextMenu.style.display = 'block';
contextMenu.setAttribute('data-item-id', itemId);
contextMenu.setAttribute('data-item-type', itemType);
// Set up buttons for different item types
setupContextMenuActions(contextMenu, itemId, itemType);
}
});
// Hide menu on click elsewhere
document.addEventListener('click', function () {
contextMenu.style.display = 'none';
});
}
function setupContextMenuActions(menu, itemId, itemType) {
// Show/hide appropriate actions based on item type
menu.querySelectorAll('[data-action]').forEach(action => {
const forType = action.getAttribute('data-for');
if (forType === 'all' || forType === itemType) {
action.style.display = 'block';
} else {
action.style.display = 'none';
}
});
}
// Initialize file upload functionality
function initializeUploadFunctionality() {
const uploadBtn = document.querySelector('a[href*="upload"]');
const fileInput = document.getElementById('file-upload');
if (uploadBtn && fileInput) {
fileInput.addEventListener('change', function (e) {
if (this.files.length) {
const formData = new FormData();
for (let i = 0; i < this.files.length; i++) {
formData.append('file', this.files[i]);
}
// Get current folder ID from URL if available
const urlParams = new URLSearchParams(window.location.search);
const folderId = urlParams.get('folder_id');
if (folderId) {
formData.append('folder_id', folderId);
}
fetch('/files/upload', {
method: 'POST',
body: formData,
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Refresh page to show new file
window.location.reload();
} else {
alert('Upload failed: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Upload failed. Please try again.');
});
}
});
}
}

View file

@ -0,0 +1,183 @@
/**
* Quick upload functionality for instant file uploads
*/
document.addEventListener('DOMContentLoaded', function () {
// Elements
const globalDropzone = document.getElementById('global-dropzone');
const uploadToast = document.getElementById('upload-toast');
const uploadProgressBar = document.getElementById('upload-toast-progress-bar');
const uploadPercentage = document.getElementById('upload-toast-percentage');
const uploadFileName = document.getElementById('upload-toast-file');
const uploadToastClose = document.getElementById('upload-toast-close');
// Get current folder ID from URL or data attribute
function getCurrentFolderId() {
// Check if we're on a folder page
const urlParams = new URLSearchParams(window.location.search);
const folderIdFromUrl = urlParams.get('folder_id');
// Check for a data attribute on the page
const folderElement = document.querySelector('[data-current-folder-id]');
const folderIdFromData = folderElement ? folderElement.dataset.currentFolderId : null;
return folderIdFromUrl || folderIdFromData || null;
}
// Show global dropzone when files are dragged over the window
window.addEventListener('dragover', function (e) {
e.preventDefault();
e.stopPropagation();
// Only show dropzone if user is on dashboard or files page
const onRelevantPage = window.location.pathname.includes('/dashboard') ||
window.location.pathname.includes('/files');
if (onRelevantPage) {
globalDropzone.classList.add('active');
}
});
// Hide dropzone when dragging leaves window
window.addEventListener('dragleave', function (e) {
e.preventDefault();
e.stopPropagation();
// Only hide if leaving the window (not entering child elements)
if (e.clientX <= 0 || e.clientY <= 0 ||
e.clientX >= window.innerWidth || e.clientY >= window.innerHeight) {
globalDropzone.classList.remove('active');
}
});
// Handle drop event for quick upload
window.addEventListener('drop', function (e) {
e.preventDefault();
e.stopPropagation();
// Hide the dropzone
globalDropzone.classList.remove('active');
// Make sure files were dropped
if (!e.dataTransfer.files || e.dataTransfer.files.length === 0) {
return;
}
// Show upload progress toast
uploadToast.classList.add('active');
// Get the current folder ID (null for root)
const currentFolderId = getCurrentFolderId();
// Upload the files
uploadFiles(e.dataTransfer.files, currentFolderId);
});
// Close upload toast
if (uploadToastClose) {
uploadToastClose.addEventListener('click', function () {
uploadToast.classList.remove('active');
});
}
// Quick upload function
function uploadFiles(files, folderId) {
// Create FormData object
const formData = new FormData();
// Add folder ID if provided
if (folderId) {
formData.append('parent_folder_id', folderId);
}
// Add all files
for (let i = 0; i < files.length; i++) {
formData.append('files[]', files[i]);
}
// Create XHR request
const xhr = new XMLHttpRequest();
// Update progress
xhr.upload.addEventListener('progress', function (e) {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
uploadProgressBar.style.width = percent + '%';
uploadPercentage.textContent = percent + '%';
if (files.length === 1) {
uploadFileName.textContent = files[0].name;
} else {
uploadFileName.textContent = `Uploading ${files.length} files...`;
}
}
});
// Handle completion
xhr.addEventListener('load', function () {
if (xhr.status >= 200 && xhr.status < 300) {
// Success - upload complete
uploadProgressBar.style.width = '100%';
uploadPercentage.textContent = '100%';
uploadFileName.textContent = 'Upload Complete!';
// Show success alert
if (typeof showAlert === 'function') {
showAlert('Files uploaded successfully!', 'success');
}
// Reload page after brief delay to show new files
setTimeout(function () {
window.location.reload();
}, 1000);
} else {
// Error
let errorMessage = 'Upload failed';
try {
const response = JSON.parse(xhr.responseText);
if (response.error) {
errorMessage = response.error;
}
} catch (e) {
// Ignore parsing error
}
uploadFileName.textContent = errorMessage;
if (typeof showAlert === 'function') {
showAlert(errorMessage, 'error');
}
}
// Hide toast after delay
setTimeout(function () {
uploadToast.classList.remove('active');
// Reset progress
setTimeout(function () {
uploadProgressBar.style.width = '0%';
uploadPercentage.textContent = '0%';
uploadFileName.textContent = 'Processing...';
}, 300);
}, 3000);
});
// Handle errors
xhr.addEventListener('error', function () {
uploadFileName.textContent = 'Network error occurred';
if (typeof showAlert === 'function') {
showAlert('Network error occurred', 'error');
}
// Hide toast after delay
setTimeout(function () {
uploadToast.classList.remove('active');
}, 3000);
});
// Set up and send the request
xhr.open('POST', '/files/upload_xhr', true);
xhr.send(formData);
}
});

62
app/static/js/theme.js Normal file
View file

@ -0,0 +1,62 @@
/**
* Theme handling functionality
*/
document.addEventListener('DOMContentLoaded', function () {
const themeToggle = document.querySelector('.theme-toggle-icon');
if (themeToggle) {
themeToggle.addEventListener('click', function () {
const currentTheme = document.documentElement.getAttribute('data-theme') || 'system';
let newTheme;
if (currentTheme === 'dark') {
newTheme = 'light';
} else {
newTheme = 'dark';
}
// Update theme
document.documentElement.setAttribute('data-theme', newTheme);
// Store preference
localStorage.setItem('theme_preference', newTheme);
// Update icon
updateThemeIcon(newTheme);
});
// Initialize theme on page load
initTheme();
}
function initTheme() {
// Get saved preference
let theme = localStorage.getItem('theme_preference');
// If no preference, check system preference
if (!theme) {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
theme = 'dark';
} else {
theme = 'light';
}
}
// Apply theme
document.documentElement.setAttribute('data-theme', theme);
// Update icon
updateThemeIcon(theme);
}
function updateThemeIcon(theme) {
const icon = document.querySelector('.theme-toggle-icon i');
if (icon) {
if (theme === 'dark') {
icon.className = 'fas fa-sun';
} else {
icon.className = 'fas fa-moon';
}
}
}
});

View file

@ -10,6 +10,9 @@ document.addEventListener('DOMContentLoaded', function () {
const fileList = document.getElementById('file-list');
const folderList = document.getElementById('folder-file-list');
// Fix to avoid darkModeToggle is null error
const darkModeToggle = document.querySelector('.theme-toggle-icon');
// Progress elements
const progressBar = document.getElementById('progress-bar');
const progressPercentage = document.getElementById('progress-percentage');

View file

@ -0,0 +1,187 @@
{% extends "base.html" %}
{% block title %}Admin Panel - Flask Files{% endblock %}
{% block content %}
<div class="container">
<div class="admin-panel">
<div class="admin-header">
<h2><i class="fas fa-cog"></i> Admin Panel</h2>
</div>
<div class="admin-section">
<h3>Database Management</h3>
<div class="admin-card">
<div class="admin-card-header">
<h4>Database Migrations</h4>
</div>
<div class="admin-card-body">
<p>Run database migrations to update the schema if needed.</p>
<button id="run-migrations-btn" class="btn primary">
<i class="fas fa-database"></i> Run Migrations
</button>
<div id="migration-result" class="mt-3" style="display: none;"></div>
</div>
</div>
<div class="admin-card mt-4">
<div class="admin-card-header">
<h4>Reset Database</h4>
</div>
<div class="admin-card-body">
<p class="text-danger">
<strong>WARNING:</strong> This will delete all data and recreate the database structure.
All files, folders, and user accounts will be permanently deleted.
</p>
<button id="reset-db-btn" class="btn danger">
<i class="fas fa-exclamation-triangle"></i> Reset Database
</button>
<div id="reset-result" class="mt-3" style="display: none;"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Confirmation Modal -->
<div id="confirm-reset-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Confirm Database Reset</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="alert warning">
<i class="fas fa-exclamation-triangle"></i>
<p>
<strong>WARNING:</strong> You are about to reset the entire database.
This action cannot be undone.
</p>
<p>All files, folders, users, and settings will be permanently deleted.</p>
</div>
<p>Type "RESET" in the box below to confirm:</p>
<input type="text" id="reset-confirm-text" class="form-control mt-3" placeholder="Type RESET to confirm">
</div>
<div class="modal-footer">
<button class="btn" id="cancel-reset-btn">Cancel</button>
<button class="btn danger" id="confirm-reset-btn" disabled>Reset Database</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function () {
const migrationsBtn = document.getElementById('run-migrations-btn');
const resultDiv = document.getElementById('migration-result');
const resetBtn = document.getElementById('reset-db-btn');
const resetResultDiv = document.getElementById('reset-result');
const resetConfirmText = document.getElementById('reset-confirm-text');
const confirmResetBtn = document.getElementById('confirm-reset-btn');
const cancelResetBtn = document.getElementById('cancel-reset-btn');
if (migrationsBtn) {
migrationsBtn.addEventListener('click', function () {
// Show loading state
migrationsBtn.disabled = true;
migrationsBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Running...';
resultDiv.style.display = 'none';
// Call the migrations endpoint
fetch('/admin/run-migrations', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
// Reset button
migrationsBtn.disabled = false;
migrationsBtn.innerHTML = '<i class="fas fa-database"></i> Run Migrations';
// Show result
resultDiv.style.display = 'block';
if (data.success) {
resultDiv.innerHTML = '<div class="alert success"><i class="fas fa-check-circle"></i> ' + data.message + '</div>';
} else {
resultDiv.innerHTML = '<div class="alert error"><i class="fas fa-exclamation-circle"></i> ' + data.error + '</div>';
}
})
.catch(error => {
// Error handling
migrationsBtn.disabled = false;
migrationsBtn.innerHTML = '<i class="fas fa-database"></i> Run Migrations';
resultDiv.style.display = 'block';
resultDiv.innerHTML = '<div class="alert error"><i class="fas fa-exclamation-circle"></i> Error: ' + error.message + '</div>';
});
});
}
if (resetBtn) {
resetBtn.addEventListener('click', function () {
// Show confirmation modal
openModal('confirm-reset-modal');
});
}
if (resetConfirmText) {
resetConfirmText.addEventListener('input', function () {
confirmResetBtn.disabled = this.value !== 'RESET';
});
}
if (cancelResetBtn) {
cancelResetBtn.addEventListener('click', function () {
closeModal('confirm-reset-modal');
});
}
if (confirmResetBtn) {
confirmResetBtn.addEventListener('click', function () {
// Close modal
closeModal('confirm-reset-modal');
// Show loading state
resetBtn.disabled = true;
resetBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Resetting...';
resetResultDiv.style.display = 'none';
// Call the reset endpoint
fetch('/admin/reset-database', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
// Reset button
resetBtn.disabled = false;
resetBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Reset Database';
// Show result
resetResultDiv.style.display = 'block';
if (data.success) {
resetResultDiv.innerHTML = '<div class="alert success"><i class="fas fa-check-circle"></i> ' + data.message + '</div>';
// Redirect to login after a delay
setTimeout(function () {
window.location.href = '/auth/login';
}, 3000);
} else {
resetResultDiv.innerHTML = '<div class="alert error"><i class="fas fa-exclamation-circle"></i> ' + data.error + '</div>';
}
})
.catch(error => {
// Error handling
resetBtn.disabled = false;
resetBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Reset Database';
resetResultDiv.style.display = 'block';
resetResultDiv.innerHTML = '<div class="alert error"><i class="fas fa-exclamation-circle"></i> Error: ' + error.message + '</div>';
});
});
}
});
</script>
{% endblock %}

View file

@ -1,666 +1,50 @@
{% extends "base.html" %}
{% block title %}User Profile - Flask Files{% endblock %}
{% block extra_css %}
<style>
.profile-container {
max-width: 800px;
margin: 0 auto;
}
.profile-tabs {
display: flex;
border-bottom: 1px solid var(--border-color);
margin-bottom: 1.5rem;
}
.profile-tab {
padding: 0.75rem 1.5rem;
cursor: pointer;
background: none;
border: none;
font-weight: 500;
border-bottom: 2px solid transparent;
}
.profile-tab.active {
color: var(--primary-color);
border-bottom-color: var(--primary-color);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.setting-group {
margin-bottom: 2rem;
}
.setting-group h3 {
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--border-color);
}
/* Modal styles */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
overflow: auto;
justify-content: center;
align-items: center;
}
.modal.visible {
display: flex;
}
.modal-content {
background: var(--card-bg);
border-radius: var(--border-radius);
width: 100%;
max-width: 500px;
box-shadow: var(--shadow-lg);
margin: 2rem;
}
/* Danger zone */
.dangerous-zone {
background-color: rgba(var(--danger-color-rgb), 0.1);
border-radius: var(--border-radius);
padding: 1.5rem;
margin-top: 2rem;
}
.danger-actions {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-top: 1rem;
}
.btn.dangerous {
background-color: var(--danger-color);
color: #fff;
}
.btn.dangerous:hover {
background-color: var(--danger-hover-color);
}
/* Theme toggle */
.theme-options {
display: flex;
gap: 0.5rem;
}
.theme-btn {
padding: 0.5rem 1rem;
border-radius: var(--border-radius);
background: var(--secondary-bg);
border: 1px solid var(--border-color);
cursor: pointer;
}
.theme-btn.active {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
</style>
{% endblock %}
{% block title %}User Profile{% endblock %}
{% block content %}
<div class="container">
<div class="profile-card">
<div class="profile-header">
<h2>User Profile</h2>
<div class="theme-toggle">
<span>Theme:</span>
<div class="theme-options">
<button class="theme-btn {% if theme_preference == 'light' %}active{% endif %}" data-theme="light">
<i class="fas fa-sun"></i> Light
</button>
<button class="theme-btn {% if theme_preference == 'dark' %}active{% endif %}" data-theme="dark">
<i class="fas fa-moon"></i> Dark
</button>
<button class="theme-btn {% if theme_preference == 'system' %}active{% endif %}"
data-theme="system">
<i class="fas fa-desktop"></i> System
</button>
<div class="row">
<div class="col-md-8 offset-md-2">
<div class="card">
<div class="card-header">
<h3>User Profile</h3>
</div>
</div>
</div>
<!-- Tab navigation -->
<div class="profile-tabs">
<button class="profile-tab active" data-tab="account">Account</button>
<button class="profile-tab" data-tab="settings">Settings</button>
</div>
<!-- Account Tab -->
<div class="tab-content active" id="account-tab">
<div class="profile-content">
<div class="profile-stats">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-file"></i>
</div>
<div class="stat-info">
<h3 class="stat-title">Files</h3>
<span class="stat-value">{{ current_user.files.filter_by(is_folder=False).count() }}</span>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-3 font-weight-bold">Username:</div>
<div class="col-md-9">{{ user.username }}</div>
</div>
<div class="row mb-3">
<div class="col-md-3 font-weight-bold">Email:</div>
<div class="col-md-9">{{ user.email }}</div>
</div>
<div class="row mb-3">
<div class="col-md-3 font-weight-bold">Member Since:</div>
<div class="col-md-9">{{ user.created_at.strftime('%Y-%m-%d') }}</div>
</div>
<div class="row mb-3">
<div class="col-md-3 font-weight-bold">Account Type:</div>
<div class="col-md-9">{% if user.is_admin %}Administrator{% else %}Regular User{% endif %}</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-folder"></i>
</div>
<div class="stat-info">
<h3 class="stat-title">Folders</h3>
<span class="stat-value">{{ current_user.files.filter_by(is_folder=True).count() }}</span>
</div>
</div>
<hr>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-share-alt"></i>
</div>
<div class="stat-info">
<h3 class="stat-title">Shares</h3>
<span class="stat-value">{{ current_user.shares.count() if hasattr(current_user, 'shares')
else 0 }}</span>
</div>
<h4 class="mt-4">Storage Summary</h4>
<div class="row mb-3">
<div class="col-md-3 font-weight-bold">Files:</div>
<div class="col-md-9">{{ user.files.count() }}</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-calendar-alt"></i>
</div>
<div class="stat-info">
<h3 class="stat-title">Member Since</h3>
<span class="stat-value">{{ current_user.created_at.strftime('%b %d, %Y') }}</span>
</div>
<div class="row mb-3">
<div class="col-md-3 font-weight-bold">Folders:</div>
<div class="col-md-9">{{ user.folders.count() }}</div>
</div>
</div>
<div class="profile-form">
<h3>Account Information</h3>
<form action="{{ url_for('auth.update_profile') }}" method="post">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" value="{{ current_user.username }}"
required>
</div>
<div class="form-group">
<label for="email">Email Address</label>
<input type="email" id="email" name="email" value="{{ current_user.email }}" required>
</div>
<div class="form-actions">
<button type="submit" class="btn primary">Save Changes</button>
</div>
</form>
<h3>Change Password</h3>
<form action="{{ url_for('auth.change_password') }}" method="post">
<div class="form-group">
<label for="current_password">Current Password</label>
<input type="password" id="current_password" name="current_password" required>
</div>
<div class="form-group">
<label for="new_password">New Password</label>
<input type="password" id="new_password" name="new_password" required>
</div>
<div class="form-group">
<label for="confirm_password">Confirm New Password</label>
<input type="password" id="confirm_password" name="confirm_password" required>
</div>
<div class="form-actions">
<button type="submit" class="btn primary">Change Password</button>
</div>
</form>
</div>
</div>
</div>
<!-- Settings Tab -->
<div class="tab-content" id="settings-tab">
<div class="settings-content">
<div class="settings-section">
<h3>Appearance</h3>
<div class="setting-group">
<div class="setting-label">File View</div>
<div class="setting-controls">
<div class="view-options">
<button class="view-btn {% if view_preference == 'grid' %}active{% endif %}"
data-view="grid">
<i class="fas fa-th"></i> Grid
</button>
<button class="view-btn {% if view_preference == 'list' %}active{% endif %}"
data-view="list">
<i class="fas fa-list"></i> List
</button>
</div>
</div>
</div>
</div>
<div class="settings-section">
<h3>Notifications</h3>
<div class="setting-group">
<div class="setting-label">Email Notifications</div>
<div class="setting-controls">
<label class="toggle">
<input type="checkbox" id="email-notifications" {% if notifications and
notifications.email %}checked{% endif %}>
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-description">
Receive email notifications about file shares and new comments
</div>
</div>
</div>
<div class="settings-section">
<h3>Privacy</h3>
<div class="setting-group">
<div class="setting-label">Public Profile</div>
<div class="setting-controls">
<label class="toggle">
<input type="checkbox" id="public-profile" {% if privacy and privacy.public_profile
%}checked{% endif %}>
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-description">
Allow others to see your profile and shared files
</div>
</div>
<div class="setting-group">
<div class="setting-label">Share Statistics</div>
<div class="setting-controls">
<label class="toggle">
<input type="checkbox" id="share-statistics" {% if privacy and privacy.share_statistics
%}checked{% endif %}>
<span class="toggle-slider"></span>
</label>
</div>
<div class="setting-description">
Collect anonymous usage statistics to improve the service
</div>
</div>
</div>
<div class="settings-section dangerous-zone">
<h3>Danger Zone</h3>
<p class="warning-text">
These actions are permanent and cannot be undone
</p>
<div class="danger-actions">
<button id="delete-files-btn" class="btn dangerous">
<i class="fas fa-trash-alt"></i> Delete All Files
</button>
<button id="delete-account-btn" class="btn dangerous">
<i class="fas fa-user-times"></i> Delete Account
</button>
</div>
<div class="card-footer">
<a href="{{ url_for('dashboard.index') }}" class="btn btn-secondary">Back to Dashboard</a>
</div>
</div>
</div>
</div>
</div>
<!-- Delete Files Confirmation Modal -->
<div id="delete-files-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Delete All Files</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<p class="warning-text">
Are you sure you want to delete all your files? This action cannot be undone.
</p>
<div class="form-group">
<label for="delete-files-confirm">Type "DELETE" to confirm</label>
<input type="text" id="delete-files-confirm">
</div>
<div class="form-actions">
<button class="btn secondary modal-cancel">Cancel</button>
<button id="confirm-delete-files" class="btn dangerous" disabled>Delete All Files</button>
</div>
</div>
</div>
</div>
<!-- Delete Account Confirmation Modal -->
<div id="delete-account-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Delete Account</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<p class="warning-text">
Are you sure you want to delete your account? All your files will be permanently deleted.
This action cannot be undone.
</p>
<div class="form-group">
<label for="delete-account-confirm">Type your username to confirm</label>
<input type="text" id="delete-account-confirm">
</div>
<div class="form-actions">
<button class="btn secondary modal-cancel">Cancel</button>
<button id="confirm-delete-account" class="btn dangerous" disabled>Delete Account</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
document.addEventListener('DOMContentLoaded', function () {
// Make sure modals are hidden on load
document.querySelectorAll('.modal').forEach(modal => {
modal.style.display = 'none';
});
// Tab switching
const tabs = document.querySelectorAll('.profile-tab');
const tabContents = document.querySelectorAll('.tab-content');
tabs.forEach(tab => {
tab.addEventListener('click', function () {
const tabId = this.dataset.tab;
// Hide all tab contents
tabContents.forEach(content => {
content.classList.remove('active');
});
// Show selected tab content
document.getElementById(`${tabId}-tab`).classList.add('active');
// Update active tab
tabs.forEach(t => t.classList.remove('active'));
this.classList.add('active');
});
});
// Theme switching functionality
const themeButtons = document.querySelectorAll('.theme-btn');
themeButtons.forEach(button => {
button.addEventListener('click', function () {
const theme = this.dataset.theme;
setTheme(theme);
// Update active button
themeButtons.forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
// Save preference
fetch('/auth/set_theme', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ theme: theme })
});
});
});
function setTheme(theme) {
const html = document.documentElement;
if (theme === 'light') {
html.setAttribute('data-theme', 'light');
} else if (theme === 'dark') {
html.setAttribute('data-theme', 'dark');
} else if (theme === 'system') {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
html.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
}
}
// View preference
const viewButtons = document.querySelectorAll('.view-btn');
viewButtons.forEach(button => {
button.addEventListener('click', function () {
const view = this.dataset.view;
// Update active button
viewButtons.forEach(btn => btn.classList.remove('active'));
this.classList.add('active');
// Save preference
fetch('/auth/set_view_preference', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ view: view })
});
});
});
// Toggle settings
const toggles = document.querySelectorAll('.toggle input[type="checkbox"]');
toggles.forEach(toggle => {
toggle.addEventListener('change', function () {
const setting = this.id;
const value = this.checked;
fetch('/auth/update_setting', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({
setting: setting,
value: value
})
});
});
});
// Password validation
const passwordForm = document.querySelector('form[action*="change_password"]');
passwordForm.addEventListener('submit', function (e) {
const newPassword = document.getElementById('new_password').value;
const confirmPassword = document.getElementById('confirm_password').value;
if (newPassword !== confirmPassword) {
e.preventDefault();
showAlert('New password and confirmation do not match', 'error');
}
});
// Delete files functionality
const deleteFilesBtn = document.getElementById('delete-files-btn');
const deleteFilesModal = document.getElementById('delete-files-modal');
const deleteFilesConfirmInput = document.getElementById('delete-files-confirm');
const confirmDeleteFilesBtn = document.getElementById('confirm-delete-files');
deleteFilesBtn.addEventListener('click', function () {
openModal(deleteFilesModal);
deleteFilesConfirmInput.value = '';
confirmDeleteFilesBtn.disabled = true;
deleteFilesConfirmInput.focus();
});
deleteFilesConfirmInput.addEventListener('input', function () {
confirmDeleteFilesBtn.disabled = this.value !== 'DELETE';
});
confirmDeleteFilesBtn.addEventListener('click', function () {
if (deleteFilesConfirmInput.value === 'DELETE') {
fetch('/auth/delete_all_files', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
closeModal(deleteFilesModal);
if (data.success) {
showAlert('All files have been deleted', 'success');
// Update file count
document.querySelectorAll('.stat-value')[0].textContent = '0';
document.querySelectorAll('.stat-value')[1].textContent = '0';
} else {
showAlert(data.error || 'Failed to delete files', 'error');
}
})
.catch(error => {
showAlert('An error occurred: ' + error, 'error');
});
}
});
// Delete account functionality
const deleteAccountBtn = document.getElementById('delete-account-btn');
const deleteAccountModal = document.getElementById('delete-account-modal');
const deleteAccountConfirmInput = document.getElementById('delete-account-confirm');
const confirmDeleteAccountBtn = document.getElementById('confirm-delete-account');
const usernameToConfirm = "{{ current_user.username }}";
deleteAccountBtn.addEventListener('click', function () {
openModal(deleteAccountModal);
deleteAccountConfirmInput.value = '';
confirmDeleteAccountBtn.disabled = true;
deleteAccountConfirmInput.focus();
});
deleteAccountConfirmInput.addEventListener('input', function () {
confirmDeleteAccountBtn.disabled = this.value !== usernameToConfirm;
});
confirmDeleteAccountBtn.addEventListener('click', function () {
if (deleteAccountConfirmInput.value === usernameToConfirm) {
fetch('/auth/delete_account', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => response.json())
.then(data => {
closeModal(deleteAccountModal);
if (data.success) {
showAlert('Your account has been deleted', 'success');
setTimeout(() => {
window.location.href = '/auth/logout';
}, 1500);
} else {
showAlert(data.error || 'Failed to delete account', 'error');
}
})
.catch(error => {
showAlert('An error occurred: ' + error, 'error');
});
}
});
// Modal utilities
function openModal(modal) {
modal.style.display = 'flex';
document.body.classList.add('modal-open');
}
function closeModal(modal) {
modal.style.display = 'none';
document.body.classList.remove('modal-open');
}
// 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').forEach(modal => {
closeModal(modal);
});
}
});
// Helper function to show 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 = `
<div class="alert-content">${message}</div>
<button class="close" aria-label="Close">&times;</button>
`;
// 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);
}
});
</script>
{% endblock %}

View file

@ -6,11 +6,26 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Flask Files{% endblock %}</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css">
<!-- Font Awesome -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modal.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/context-menu.css') }}">
<!-- Classless CSS Framework -->
<!-- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/digitallytailored/classless@latest/classless.min.css"> -->
<!-- Custom CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/modal.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/context-menu.css') }}">
<!-- Font Awesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
@ -18,95 +33,131 @@
{% block extra_css %}{% endblock %}
<!-- JavaScript -->
<script>
document.addEventListener('DOMContentLoaded', function () {
// Dark mode toggle button
const darkModeToggle = document.getElementById('darkModeToggle');
function setColorScheme(scheme) {
document.documentElement.setAttribute('color-scheme', scheme);
localStorage.setItem('color-scheme', scheme);
}
function getColorScheme() {
let scheme = localStorage.getItem('color-scheme');
if (scheme) {
return scheme;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
setColorScheme(getColorScheme());
darkModeToggle.addEventListener('click', function () {
const newScheme = getColorScheme() === 'dark' ? 'light' : 'dark';
setColorScheme(newScheme);
});
darkModeToggle.checked = getColorScheme() === 'dark';
});
</script>
{% block extra_js %}{% endblock %}
</head>
<body>
<header class="navbar">
<div class="navbar-brand">
<a href="{{ url_for('dashboard.index') }}">Flask Files</a>
{% if current_user.is_authenticated %}
<!-- Global drop zone overlay - hidden by default -->
<div id="global-dropzone" class="global-dropzone">
<div class="dropzone-content">
<div class="dropzone-icon">
<i class="fas fa-cloud-upload-alt fa-3x"></i>
</div>
<h3>Drop Files to Upload</h3>
<p>Files will be instantly uploaded to current folder</p>
</div>
<nav class="navbar-menu">
{% if current_user.is_authenticated %}
<a href="{{ url_for('dashboard.index') }}" class="nav-item">
<i class="fas fa-tachometer-alt"></i> Dashboard
</a>
<a href="{{ url_for('files.browser') }}" class="nav-item">
<i class="fas fa-folder"></i> Files
</a>
<a href="{{ url_for('auth.profile') }}" class="nav-item">
<i class="fas fa-user"></i> {{ current_user.username }}
</a>
<a href="{{ url_for('auth.logout') }}" class="nav-item">
<i class="fas fa-sign-out-alt"></i> Logout
</a>
<div class="theme-toggle-icon">
<i class="fas fa-moon"></i>
</div>
{% else %}
<a href="{{ url_for('auth.login') }}" class="nav-item">
<i class="fas fa-sign-in-alt"></i> Login
</a>
<a href="{{ url_for('auth.register') }}" class="nav-item">
<i class="fas fa-user-plus"></i> Register
</a>
<div class="theme-toggle-icon">
<i class="fas fa-moon"></i>
</div>
{% endif %}
</nav>
</header>
</div>
<main>
<!-- Global upload progress toast -->
<div id="upload-toast" class="upload-toast">
<div class="upload-toast-header">
<i class="fas fa-cloud-upload-alt"></i>
<span>Uploading Files</span>
<button id="upload-toast-close" class="upload-toast-close">&times;</button>
</div>
<div class="upload-toast-body">
<div class="upload-toast-progress-info">
<span id="upload-toast-file">Processing...</span>
<span id="upload-toast-percentage">0%</span>
</div>
<div class="upload-toast-progress-bar-container">
<div id="upload-toast-progress-bar" class="upload-toast-progress-bar"></div>
</div>
</div>
</div>
{% endif %}
<!-- Navigation -->
<nav class="navbar navbar-expand-md navbar-dark bg-dark fixed-top">
<div class="container">
<a class="navbar-brand" href="{{ url_for('dashboard.index') }}">
<i class="fas fa-folder-open mr-2"></i>
Flask Files
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarContent">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarContent">
<ul class="navbar-nav ml-auto">
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('dashboard.index') }}">
<i class="fas fa-tachometer-alt mr-1"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('files.browser') }}">
<i class="fas fa-folder mr-1"></i> My Files
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('files.upload') }}">
<i class="fas fa-upload mr-1"></i> Upload
</a>
</li>
{% if current_user.is_admin %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('admin.index') }}">
<i class="fas fa-cog mr-1"></i> Admin
</a>
</li>
{% endif %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" data-toggle="dropdown">
<i class="fas fa-user-circle mr-1"></i> {{ current_user.username }}
</a>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="{{ url_for('auth.profile') }}">
<i class="fas fa-id-card mr-2"></i> Profile
</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="{{ url_for('auth.logout') }}">
<i class="fas fa-sign-out-alt mr-2"></i> Logout
</a>
</div>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">
<i class="fas fa-sign-in-alt mr-1"></i> Login
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.register') }}">
<i class="fas fa-user-plus mr-1"></i> Register
</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<!-- Flash Messages -->
<div class="container mt-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<section class="alerts">
{% for category, message in messages %}
<div class="alert {{ category }}">
{{ message }}
<button class="close" aria-label="Close">&times;</button>
</div>
{% endfor %}
</section>
{% for category, message in messages %}
<div class="alert alert-{{ category if category != 'message' else 'info' }} alert-dismissible fade show">
{{ message }}
<button type="button" class="close" data-dismiss="alert">&times;</button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<section class="content">
{% block content %}{% endblock %}
</section>
<!-- Main Content -->
<main role="main" class="container mt-3">
{% block content %}{% endblock %}
</main>
<footer>
<div class="container">
<p>&copy; {{ now.year }} Flask Files. All rights reserved.</p>
<!-- Footer -->
<footer class="footer mt-auto py-3 bg-light">
<div class="container text-center">
<span class="text-muted">&copy; 2023 Flask Files. All rights reserved.</span>
</div>
</footer>
@ -161,6 +212,19 @@
<!-- Common JS file with shared functions -->
<script src="{{ url_for('static', filename='js/common.js') }}"></script>
<script src="{{ url_for('static', filename='js/theme.js') }}"></script>
<script src="{{ url_for('static', filename='js/context-menu.js') }}"></script>
{% if current_user.is_authenticated %}
<script src="{{ url_for('static', filename='js/quick-upload.js') }}"></script>
{% endif %}
<!-- JavaScript -->
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Custom JavaScript -->
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
{% block scripts %}{% endblock %}
</body>

View file

@ -0,0 +1,105 @@
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<div class="container">
<h1 class="mb-4">Dashboard</h1>
<!-- Storage Summary Cards -->
<div class="row">
<div class="col-md-4 mb-4">
<div class="card border-primary">
<div class="card-body">
<div class="row">
<div class="col-md-4 text-center">
<i class="fas fa-file fa-3x text-primary"></i>
</div>
<div class="col-md-8">
<h5 class="card-title">Files</h5>
<h3>{{ file_count }}</h3>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card border-success">
<div class="card-body">
<div class="row">
<div class="col-md-4 text-center">
<i class="fas fa-folder fa-3x text-success"></i>
</div>
<div class="col-md-8">
<h5 class="card-title">Folders</h5>
<h3>{{ folder_count }}</h3>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="card border-info">
<div class="card-body">
<div class="row">
<div class="col-md-4 text-center">
<i class="fas fa-hdd fa-3x text-info"></i>
</div>
<div class="col-md-8">
<h5 class="card-title">Storage Used</h5>
<h3>{{ storage_used_formatted }}</h3>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Files -->
<div class="card mt-4">
<div class="card-header bg-light">
<h4 class="mb-0">Recent Files</h4>
</div>
<div class="card-body">
{% if recent_files %}
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Type</th>
<th>Size</th>
<th>Uploaded</th>
</tr>
</thead>
<tbody>
{% for file in recent_files %}
<tr>
<td>
<i class="fas {{ file.icon_class }} mr-2"></i>
<a href="{{ url_for('files.download', file_id=file.id) }}">{{ file.name }}</a>
</td>
<td>{{ file.type }}</td>
<td>{{ file.size|filesizeformat }}</td>
<td>{{ file.created_at.strftime('%Y-%m-%d %H:%M') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="alert alert-info">
You haven't uploaded any files yet.
<a href="{{ url_for('files.upload') }}" class="alert-link">Upload your first file</a>
</div>
{% endif %}
</div>
<div class="card-footer">
<a href="{{ url_for('files.browser') }}" class="btn btn-primary">
<i class="fas fa-folder-open mr-1"></i> Browse Files
</a>
<a href="{{ url_for('files.upload') }}" class="btn btn-success">
<i class="fas fa-upload mr-1"></i> Upload Files
</a>
</div>
</div>
</div>
{% endblock %}

View file

@ -4,410 +4,7 @@
{% block extra_css %}
<style>
.browser-container {
background: var(--card-bg);
border-radius: var(--border-radius);
padding: 1.5rem;
margin-bottom: 2rem;
box-shadow: var(--shadow-sm);
}
.browser-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
flex-wrap: wrap;
gap: 1rem;
}
.browser-title {
display: flex;
align-items: center;
gap: 0.5rem;
}
.browser-actions {
display: flex;
gap: 0.5rem;
align-items: center;
}
.breadcrumbs {
display: flex;
flex-wrap: wrap;
margin-bottom: 1.5rem;
background: var(--bg-light);
padding: 0.5rem 1rem;
border-radius: var(--border-radius-sm);
}
.breadcrumb-item {
display: flex;
align-items: center;
}
.breadcrumb-separator {
margin: 0 0.5rem;
color: var(--text-muted);
}
.view-toggle {
display: flex;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
overflow: hidden;
margin-left: 0.5rem;
}
.view-btn {
border: none;
background: var(--card-bg);
padding: 0.5rem;
cursor: pointer;
color: var(--text-muted);
}
.view-btn.active {
background: var(--primary-color);
color: white;
}
.filter-bar {
display: flex;
margin-bottom: 1rem;
gap: 0.5rem;
flex-wrap: wrap;
}
.search-bar {
flex-grow: 1;
position: relative;
}
.search-bar input {
width: 100%;
padding: 0.5rem 1rem 0.5rem 2.5rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
background: var(--bg-light);
}
.search-icon {
position: absolute;
left: 0.75rem;
top: 0.75rem;
color: var(--text-muted);
pointer-events: none;
}
.sort-dropdown {
position: relative;
}
.sort-dropdown-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
background: var(--bg-light);
cursor: pointer;
}
.sort-dropdown-menu {
position: absolute;
top: 100%;
right: 0;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
min-width: 200px;
z-index: 10;
box-shadow: var(--shadow-md);
display: none;
margin-top: 0.25rem;
}
.sort-dropdown-menu.show {
display: block;
}
.sort-option {
padding: 0.5rem 1rem;
cursor: pointer;
}
.sort-option:hover {
background: var(--bg-hover);
}
.sort-option.active {
color: var(--primary-color);
font-weight: 500;
}
.files-container {
min-height: 200px;
position: relative;
}
.loading-indicator {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(var(--card-bg-rgb), 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 5;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
}
.loading-indicator.show {
opacity: 1;
pointer-events: auto;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(var(--primary-color-rgb), 0.3);
border-radius: 50%;
border-top-color: var(--primary-color);
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Grid view */
.files-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 1rem;
}
.folder-item,
.file-item {
display: flex;
flex-direction: column;
padding: 1rem;
border-radius: var(--border-radius-sm);
border: 1px solid var(--border-color);
transition: all 0.2s ease;
position: relative;
text-decoration: none;
color: var(--text-color);
}
.folder-item:hover,
.file-item:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-sm);
background: var(--bg-hover);
}
.item-icon {
font-size: 3rem;
margin-bottom: 0.75rem;
text-align: center;
color: var(--primary-color);
}
.folder-item .item-icon {
color: #ffc107;
}
.item-info {
flex: 1;
display: flex;
flex-direction: column;
}
.item-name {
font-weight: 500;
margin-bottom: 0.25rem;
word-break: break-word;
text-align: center;
}
.item-details {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
color: var(--text-muted);
text-align: center;
}
/* List view */
.files-list {
display: flex;
flex-direction: column;
}
.list-view .folder-item,
.list-view .file-item {
flex-direction: row;
align-items: center;
padding: 0.75rem 1rem;
margin-bottom: 0.5rem;
}
.list-view .item-icon {
font-size: 1.5rem;
margin-bottom: 0;
margin-right: 1rem;
width: 1.5rem;
text-align: center;
}
.list-view .item-info {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.list-view .item-name {
margin-bottom: 0;
text-align: left;
}
.list-view .item-details {
margin-left: auto;
min-width: 200px;
justify-content: flex-end;
}
.list-view .item-date {
margin-left: 1rem;
}
/* Empty folder state */
.empty-folder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 1rem;
text-align: center;
}
.empty-icon {
font-size: 3rem;
color: var(--text-muted);
margin-bottom: 1rem;
}
.empty-message h3 {
margin-bottom: 0.5rem;
font-weight: 500;
}
.empty-message p {
color: var(--text-muted);
margin-bottom: 1.5rem;
}
.empty-actions {
display: flex;
gap: 0.5rem;
}
/* Context menu */
.context-menu {
position: fixed;
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: var(--border-radius-sm);
box-shadow: var(--shadow-md);
z-index: 100;
min-width: 180px;
padding: 0.5rem 0;
}
.context-menu-item {
padding: 0.5rem 1rem;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
}
.context-menu-item:hover {
background: var(--bg-hover);
}
.context-menu-item.danger {
color: var(--danger-color);
}
/* Responsive */
@media (max-width: 768px) {
.browser-header {
flex-direction: column;
align-items: flex-start;
}
.browser-actions {
width: 100%;
justify-content: space-between;
}
.filter-bar {
flex-direction: column;
}
.files-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
.list-view .item-info {
flex-direction: column;
align-items: flex-start;
}
.list-view .item-details {
margin-left: 0;
margin-top: 0.25rem;
}
}
/* Modal fixes - ensure they're hidden by default */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
overflow: auto;
justify-content: center;
align-items: center;
}
.modal.visible {
display: flex;
}
.modal-content {
background: var(--card-bg);
border-radius: var(--border-radius);
width: 100%;
max-width: 500px;
box-shadow: var(--shadow-lg);
margin: 2rem;
}
/* Additional page-specific styles if needed */
</style>
{% endblock %}
@ -420,17 +17,15 @@
<h2>{% if current_folder %}{{ current_folder.name }}{% else %}My Files{% endif %}</h2>
</div>
<div class="browser-actions">
<input type="text" id="search-input" class="search-input" placeholder="Search files...">
<button id="search-btn" class="btn">
<i class="fas fa-search"></i>
</button>
<a href="{% if current_folder %}{{ url_for('files.upload', folder_id=current_folder.id) }}{% else %}{{ url_for('files.upload') }}{% endif %}"
class="btn primary">
<i class="fas fa-upload"></i> Upload
</a>
<button id="new-folder-btn" class="btn secondary">
<i class="fas fa-folder-plus"></i> New Folder
</button>
<div class="search-container">
<form action="{{ url_for('files.browser') }}" method="get">
<input type="text" name="q" placeholder="Search files..."
value="{{ request.args.get('q', '') }}">
<button type="submit" class="search-btn">
<i class="fas fa-search"></i>
</button>
</form>
</div>
<div class="view-toggle">
<button id="grid-view-btn" class="view-btn active" title="Grid View">
<i class="fas fa-th"></i>
@ -439,6 +34,13 @@
<i class="fas fa-list"></i>
</button>
</div>
<a href="{% if current_folder %}{{ url_for('files.upload', folder_id=current_folder.id) }}{% else %}{{ url_for('files.upload') }}{% endif %}"
class="btn primary">
<i class="fas fa-upload"></i> Upload
</a>
<button id="new-folder-btn" class="btn secondary">
<i class="fas fa-folder-plus"></i> New Folder
</button>
</div>
</div>
@ -473,12 +75,12 @@
<form id="new-folder-form" action="{{ url_for('files.create_folder') }}" method="post">
<div class="form-group">
<label for="folder-name">Folder Name</label>
<input type="text" id="folder-name" name="name" required>
<input type="text" id="folder-name" name="name" required placeholder="Enter folder name">
<input type="hidden" id="parent-folder-id" name="parent_id"
value="{% if current_folder %}{{ current_folder.id }}{% endif %}">
</div>
<div class="form-actions">
<button type="button" class="btn secondary modal-cancel">Cancel</button>
<button type="button" class="btn modal-cancel">Cancel</button>
<button type="submit" class="btn primary">Create Folder</button>
</div>
</form>
@ -486,133 +88,34 @@
</div>
</div>
<!-- File Actions Modal -->
<div id="file-actions-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3 id="file-name-header">File Actions</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="file-actions-list">
<a id="download-action" href="#" class="file-action">
<i class="fas fa-download"></i> Download
</a>
<a id="share-action" href="#" class="file-action">
<i class="fas fa-share-alt"></i> Share
</a>
<button id="rename-action" class="file-action">
<i class="fas fa-edit"></i> Rename
</button>
<button id="delete-action" class="file-action dangerous">
<i class="fas fa-trash-alt"></i> Delete
</button>
</div>
</div>
<!-- Context Menu -->
<div id="context-menu" class="context-menu">
<div class="context-menu-item" data-action="open" data-for="folder">
<i class="fas fa-folder-open"></i> Open
</div>
<div class="context-menu-item" data-action="download" data-for="file">
<i class="fas fa-download"></i> Download
</div>
<div class="context-menu-item" data-action="share" data-for="file">
<i class="fas fa-share-alt"></i> Share
</div>
<div class="context-menu-item" data-action="rename" data-for="all">
<i class="fas fa-pencil-alt"></i> Rename
</div>
<div class="context-menu-item" data-action="delete" data-for="all">
<i class="fas fa-trash"></i> Delete
</div>
</div>
<!-- Rename Modal -->
<div id="rename-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Rename Item</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<form id="rename-form">
<div class="form-group">
<label for="new-name">New Name</label>
<input type="text" id="new-name" name="name" required>
<input type="hidden" id="rename-item-id" name="item_id">
</div>
<div class="form-actions">
<button type="button" class="btn secondary modal-cancel">Cancel</button>
<button type="submit" class="btn primary">Rename</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div id="delete-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3>Confirm Deletion</h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<p id="delete-confirmation-message">Are you sure you want to delete this item? This action cannot be undone.
</p>
<div class="form-actions">
<button class="btn secondary modal-cancel">Cancel</button>
<button id="confirm-delete" class="btn dangerous">Delete</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{% block extra_js %}
<script>
// Add folder entrance animation
document.addEventListener('DOMContentLoaded', function () {
// Setup modals on page load
setupModals();
// Initialize variables
const filesContainer = document.getElementById('files-container');
const gridViewBtn = document.getElementById('grid-view-btn');
const listViewBtn = document.getElementById('list-view-btn');
const newFolderBtn = document.getElementById('new-folder-btn');
let selectedItemId = null;
// Button event listeners
if (gridViewBtn && listViewBtn) {
gridViewBtn.addEventListener('click', function () {
filesContainer.className = 'files-container grid-view';
gridViewBtn.classList.add('active');
listViewBtn.classList.remove('active');
// Save preference
localStorage.setItem('view_preference', 'grid');
});
listViewBtn.addEventListener('click', function () {
filesContainer.className = 'files-container list-view';
listViewBtn.classList.add('active');
gridViewBtn.classList.remove('active');
// Save preference
localStorage.setItem('view_preference', 'list');
});
if (filesContainer) {
filesContainer.classList.add('folder-enter-active');
}
// Apply saved view preference
const savedView = localStorage.getItem('view_preference');
if (savedView === 'list') {
filesContainer.className = 'files-container list-view';
if (listViewBtn && gridViewBtn) {
listViewBtn.classList.add('active');
gridViewBtn.classList.remove('active');
}
}
// New folder button
if (newFolderBtn) {
newFolderBtn.addEventListener('click', function () {
openModal('new-folder-modal');
document.getElementById('folder-name').focus();
});
}
// Setup file item event listeners
function setupFileListeners() {
// ... your existing file listeners ...
}
// Initial setup
setupFileListeners();
});
</script>
{% endblock %}
{% endblock %}

View file

@ -9,10 +9,19 @@
<div class="item-info">
<div class="item-name">{{ folder.name }}</div>
<div class="item-details">
<span class="item-count">{{ folder.children.count() }} items</span>
<span class="item-count">{{ folder.children.count() if folder.children is defined else
folder.files.count() }} items</span>
<span class="item-date">{{ folder.updated_at.strftime('%Y-%m-%d') }}</span>
</div>
</div>
<div class="file-actions">
<button class="action-btn edit" title="Rename">
<i class="fas fa-pencil-alt"></i>
</button>
<button class="action-btn delete" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</a>
{% endfor %}
{% endif %}
@ -21,15 +30,26 @@
{% for file in files %}
<a href="{{ url_for('files.download', file_id=file.id) }}" class="file-item" data-id="{{ file.id }}">
<div class="item-icon">
<i class="fas {{ file_icon(file.mime_type, file.name) }}"></i>
<i class="fas {{ file.icon_class }}"></i>
</div>
<div class="item-info">
<div class="item-name">{{ file.name }}</div>
<div class="item-details">
<span class="item-size">{{ format_size(file.size) }}</span>
<span class="item-size">{{ file.size|filesizeformat }}</span>
<span class="item-date">{{ file.updated_at.strftime('%Y-%m-%d') }}</span>
</div>
</div>
<div class="file-actions">
<button class="action-btn download" title="Download">
<i class="fas fa-download"></i>
</button>
<button class="action-btn share" title="Share">
<i class="fas fa-share-alt"></i>
</button>
<button class="action-btn delete" title="Delete">
<i class="fas fa-trash"></i>
</button>
</div>
</a>
{% endfor %}
{% endif %}

View file

@ -145,7 +145,7 @@
<i class="fas fa-file-upload"></i> File Upload
</button>
<button class="upload-tab" data-tab="folder-upload-tab">
<i class="fas fa-folder-upload"></i> Folder Upload
<i class="fas fa-folder-plus"></i> Folder Upload
</button>
</div>
@ -158,7 +158,7 @@
<p>Or click to browse your device</p>
<button id="file-select-btn" class="btn primary">Select Files</button>
<form id="file-upload-form" method="post" enctype="multipart/form-data">
<input type="hidden" name="folder_id"
<input type="hidden" name="parent_folder_id"
value="{% if parent_folder %}{{ parent_folder.id }}{% endif %}">
<input type="file" id="file-input" name="files[]" multiple style="display: none;">
</form>
@ -191,7 +191,7 @@
<p>Or click to browse your device</p>
<button id="folder-select-btn" class="btn primary">Select Folder</button>
<form id="folder-upload-form" method="post" enctype="multipart/form-data">
<input type="hidden" name="folder_id"
<input type="hidden" name="parent_folder_id"
value="{% if parent_folder %}{{ parent_folder.id }}{% endif %}">
<input type="file" id="folder-input" name="files[]" webkitdirectory directory multiple
style="display: none;">

59
app/utils/reset_db.py Normal file
View file

@ -0,0 +1,59 @@
"""
Database reset utility - USE WITH CAUTION
This will delete and recreate your entire database
"""
import os
import logging
from flask import Flask
from .. import create_app, db
from ..models import User, Folder, File, Download
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def reset_database():
"""Reset the entire database and recreate tables"""
app = create_app()
with app.app_context():
try:
# Drop all tables
logger.warning("Dropping all database tables...")
db.drop_all()
logger.info("All tables dropped successfully")
# Recreate tables based on models
logger.info("Recreating database tables...")
db.create_all()
logger.info("Database tables created successfully")
# Create admin user if needed
if User.query.filter_by(username='admin').first() is None:
admin = User(username='admin', email='admin@example.com', is_admin=True)
admin.password = 'adminpassword' # Set to a secure password in production
db.session.add(admin)
db.session.commit()
logger.info("Created admin user")
# Clear uploads folder
uploads_folder = app.config['UPLOAD_FOLDER']
if os.path.exists(uploads_folder):
logger.info(f"Clearing uploads folder: {uploads_folder}")
for file in os.listdir(uploads_folder):
file_path = os.path.join(uploads_folder, file)
try:
if os.path.isfile(file_path):
os.unlink(file_path)
elif os.path.isdir(file_path):
import shutil
shutil.rmtree(file_path)
except Exception as e:
logger.error(f"Error removing {file_path}: {e}")
return True
except Exception as e:
logger.error(f"Error resetting database: {e}")
return False
if __name__ == "__main__":
reset_database()

161
reset_app.py Normal file
View file

@ -0,0 +1,161 @@
#!/usr/bin/env python3
"""
Complete application reset script
Removes database, clears cache files, and reinitializes everything
"""
import os
import sys
import shutil
import logging
import importlib
from pathlib import Path
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger("app-reset")
# Application paths
DB_FILES = ["app.db", "instance/app.db"]
UPLOADS_FOLDER = "uploads"
PYCACHE_PATTERN = "**/__pycache__"
def confirm_reset():
"""Get user confirmation before proceeding"""
print("\n⚠️ WARNING: This will completely reset the application ⚠️")
print("📢 All data will be lost, including:")
print(" - Database and all its tables")
print(" - All uploaded files")
print(" - Cache files and temporary data")
print("\nAre you absolutely sure you want to continue?")
return input("Type 'yes' to confirm: ").lower() == 'yes'
def remove_database_files():
"""Remove all database files"""
success = True
for db_file in DB_FILES:
if os.path.exists(db_file):
try:
logger.info(f"Removing database file: {db_file}")
os.remove(db_file)
except Exception as e:
logger.error(f"Failed to remove {db_file}: {e}")
success = False
return success
def clear_uploads_directory():
"""Clear all uploaded files but keep the directory"""
if not os.path.exists(UPLOADS_FOLDER):
logger.info(f"Creating uploads folder: {UPLOADS_FOLDER}")
os.makedirs(UPLOADS_FOLDER, exist_ok=True)
return True
try:
logger.info(f"Clearing uploads folder: {UPLOADS_FOLDER}")
for item in os.listdir(UPLOADS_FOLDER):
path = os.path.join(UPLOADS_FOLDER, item)
if os.path.isfile(path):
os.remove(path)
elif os.path.isdir(path):
shutil.rmtree(path)
return True
except Exception as e:
logger.error(f"Failed to clear uploads directory: {e}")
return False
def clear_pycache_files():
"""Clear all __pycache__ directories"""
try:
logger.info("Clearing Python cache files...")
count = 0
for cache_dir in Path('.').glob(PYCACHE_PATTERN):
if cache_dir.is_dir():
shutil.rmtree(cache_dir)
count += 1
logger.info(f"Removed {count} __pycache__ directories")
return True
except Exception as e:
logger.error(f"Failed to clear cache files: {e}")
return False
def initialize_application(create_admin=False):
"""Initialize the application with fresh database"""
try:
logger.info("Initializing Flask application...")
# Force reload the app module
if 'app' in sys.modules:
importlib.reload(sys.modules['app'])
# Import and create app
from app import create_app, db
app = create_app()
with app.app_context():
logger.info("Creating database tables...")
db.create_all()
# Create admin user if requested
if create_admin:
from app.models import User, Share
if User.query.filter_by(username='admin').first() is None:
admin = User(username='admin', email='admin@example.com', is_admin=True)
admin.password = 'adminpassword'
db.session.add(admin)
db.session.commit()
logger.info("Created admin user (username: admin, password: adminpassword)")
else:
logger.info("Admin user already exists")
else:
logger.info("Skipping admin user creation as requested")
logger.info("Application initialized successfully")
return True
except Exception as e:
logger.error(f"Failed to initialize application: {e}")
return False
def main():
"""Main reset procedure"""
print("\n🔄 Flask Files Application Reset Tool 🔄\n")
if not confirm_reset():
print("\n❌ Reset cancelled by user")
return False
create_admin = input("\nCreate admin user? (yes/no): ").lower() == 'yes'
print("\n🚀 Starting reset process...\n")
# Step 1: Remove database files
if remove_database_files():
logger.info("✅ Database files removed successfully")
else:
logger.error("❌ Failed to remove some database files")
# Step 2: Clear uploads directory
if clear_uploads_directory():
logger.info("✅ Uploads directory cleared successfully")
else:
logger.error("❌ Failed to clear uploads directory")
# Step 3: Clear cache files
if clear_pycache_files():
logger.info("✅ Python cache files cleared successfully")
else:
logger.error("❌ Failed to clear cache files")
# Step 4: Initialize application
if initialize_application(create_admin):
logger.info("✅ Application initialized successfully")
else:
logger.error("❌ Failed to initialize application")
return False
print("\n✨ Reset completed successfully! ✨")
print("\nYou can now start the application with 'python run.py'")
if create_admin:
print("Default admin credentials: username=admin, password=adminpassword")
return True
if __name__ == "__main__":
sys.exit(0 if main() else 1)