Code from github to self hosted with git tea in AWS
This commit is contained in:
40
mini-dropbox/CODE/app/__init__.py
Normal file
40
mini-dropbox/CODE/app/__init__.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# === FILE: app/__init__.py ===
|
||||
from flask import Flask, render_template
|
||||
from app.config import Config
|
||||
|
||||
|
||||
def create_app():
|
||||
"""Flask application factory."""
|
||||
app = Flask(__name__)
|
||||
|
||||
# Load configuration
|
||||
app.config['SECRET_KEY'] = Config.FLASK_SECRET_KEY
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||
|
||||
# Initialize OAuth
|
||||
from app.auth import init_oauth
|
||||
init_oauth(app)
|
||||
|
||||
# Register blueprints
|
||||
from app.auth import auth_bp
|
||||
from app.files import files_bp
|
||||
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(files_bp)
|
||||
|
||||
# Root route - redirect to login page
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('login.html')
|
||||
|
||||
# Error handlers
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
return render_template('login.html'), 404
|
||||
|
||||
@app.errorhandler(500)
|
||||
def server_error(e):
|
||||
return "Internal Server Error", 500
|
||||
|
||||
return app
|
||||
97
mini-dropbox/CODE/app/auth.py
Normal file
97
mini-dropbox/CODE/app/auth.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# === FILE: app/auth.py ===
|
||||
from functools import wraps
|
||||
from flask import Blueprint, session, redirect, url_for, request, flash
|
||||
from authlib.integrations.flask_client import OAuth
|
||||
from app.config import Config
|
||||
from app import db
|
||||
|
||||
|
||||
auth_bp = Blueprint('auth', __name__, url_prefix='/auth')
|
||||
|
||||
# OAuth client will be initialized in __init__.py
|
||||
oauth = None
|
||||
|
||||
|
||||
def init_oauth(app):
|
||||
"""Initialize OAuth with the Flask app."""
|
||||
global oauth
|
||||
oauth = OAuth(app)
|
||||
oauth.register(
|
||||
name='google',
|
||||
client_id=Config.GOOGLE_CLIENT_ID,
|
||||
client_secret=Config.GOOGLE_CLIENT_SECRET,
|
||||
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
|
||||
client_kwargs={
|
||||
'scope': 'openid email profile'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def login_required(f):
|
||||
"""Decorator to protect routes that require authentication."""
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if 'user' not in session:
|
||||
flash('Please log in to access this page.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
@auth_bp.route('/login')
|
||||
def login():
|
||||
"""Redirect to Google OAuth login page."""
|
||||
redirect_uri = Config.GOOGLE_REDIRECT_URI
|
||||
return oauth.google.authorize_redirect(redirect_uri)
|
||||
|
||||
|
||||
@auth_bp.route('/callback')
|
||||
def callback():
|
||||
"""Handle Google OAuth callback."""
|
||||
try:
|
||||
# Exchange authorization code for access token
|
||||
token = oauth.google.authorize_access_token()
|
||||
|
||||
# Get user info from Google
|
||||
userinfo = token.get('userinfo')
|
||||
|
||||
if not userinfo:
|
||||
flash('Failed to get user information from Google.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
# Extract user data
|
||||
google_id = userinfo.get('sub')
|
||||
email = userinfo.get('email')
|
||||
name = userinfo.get('name')
|
||||
profile_picture = userinfo.get('picture')
|
||||
|
||||
# Get or create user in database
|
||||
user = db.get_or_create_user(google_id, email, name, profile_picture)
|
||||
|
||||
if not user:
|
||||
flash('Failed to create user account.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
# Store user in session
|
||||
session['user'] = {
|
||||
'id': user['id'],
|
||||
'email': user['email'],
|
||||
'name': user['name'],
|
||||
'profile_picture': user['profile_picture']
|
||||
}
|
||||
|
||||
flash(f'Welcome back, {user["name"]}!', 'success')
|
||||
return redirect(url_for('files.dashboard'))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error during OAuth callback: {e}")
|
||||
flash('An error occurred during login. Please try again.', 'error')
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
|
||||
@auth_bp.route('/logout')
|
||||
def logout():
|
||||
"""Log out the current user."""
|
||||
session.clear()
|
||||
flash('You have been logged out successfully.', 'success')
|
||||
return redirect(url_for('auth.login'))
|
||||
27
mini-dropbox/CODE/app/config.py
Normal file
27
mini-dropbox/CODE/app/config.py
Normal file
@@ -0,0 +1,27 @@
|
||||
# === FILE: app/config.py ===
|
||||
import os
|
||||
|
||||
|
||||
class Config:
|
||||
"""Application configuration loaded from environment variables."""
|
||||
|
||||
# Flask settings
|
||||
FLASK_SECRET_KEY = os.environ.get('FLASK_SECRET_KEY', 'dev-secret-key-change-in-production')
|
||||
FLASK_ENV = os.environ.get('FLASK_ENV', 'production')
|
||||
|
||||
# Google OAuth settings
|
||||
GOOGLE_CLIENT_ID = os.environ.get('GOOGLE_CLIENT_ID')
|
||||
GOOGLE_CLIENT_SECRET = os.environ.get('GOOGLE_CLIENT_SECRET')
|
||||
GOOGLE_REDIRECT_URI = os.environ.get('GOOGLE_REDIRECT_URI')
|
||||
|
||||
# AWS settings
|
||||
AWS_REGION = os.environ.get('AWS_REGION', 'us-east-1')
|
||||
S3_BUCKET_NAME = os.environ.get('S3_BUCKET_NAME')
|
||||
CLOUDFRONT_DOMAIN = os.environ.get('CLOUDFRONT_DOMAIN')
|
||||
|
||||
# Database settings
|
||||
DB_HOST = os.environ.get('DB_HOST')
|
||||
DB_PORT = os.environ.get('DB_PORT', '5432')
|
||||
DB_NAME = os.environ.get('DB_NAME', 'clouddrive')
|
||||
DB_USER = os.environ.get('DB_USER')
|
||||
DB_PASSWORD = os.environ.get('DB_PASSWORD')
|
||||
270
mini-dropbox/CODE/app/db.py
Normal file
270
mini-dropbox/CODE/app/db.py
Normal file
@@ -0,0 +1,270 @@
|
||||
# === FILE: app/db.py ===
|
||||
import psycopg
|
||||
from psycopg.rows import dict_row
|
||||
from app.config import Config
|
||||
|
||||
|
||||
def get_connection():
|
||||
"""Create and return a PostgreSQL database connection."""
|
||||
conn = psycopg.connect(
|
||||
host=Config.DB_HOST,
|
||||
port=Config.DB_PORT,
|
||||
dbname=Config.DB_NAME,
|
||||
user=Config.DB_USER,
|
||||
password=Config.DB_PASSWORD
|
||||
)
|
||||
return conn
|
||||
|
||||
|
||||
def init_db():
|
||||
"""Initialize the database by creating tables from schema.sql."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Read and execute schema.sql
|
||||
with open('sql/schema.sql', 'r') as f:
|
||||
schema = f.read()
|
||||
cursor.execute(schema)
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
print("Database initialized successfully")
|
||||
except Exception as e:
|
||||
print(f"Error initializing database: {e}")
|
||||
if conn:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_user_by_google_id(google_id):
|
||||
"""Fetch user by Google ID."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor(row_factory=dict_row)
|
||||
|
||||
cursor.execute(
|
||||
"SELECT * FROM users WHERE google_id = %s",
|
||||
(google_id,)
|
||||
)
|
||||
user = cursor.fetchone()
|
||||
cursor.close()
|
||||
|
||||
return dict(user) if user else None
|
||||
except Exception as e:
|
||||
print(f"Error fetching user: {e}")
|
||||
return None
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def create_user(google_id, email, name, profile_picture):
|
||||
"""Create a new user and return user dict."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor(row_factory=dict_row)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO users (google_id, email, name, profile_picture)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
RETURNING *
|
||||
""",
|
||||
(google_id, email, name, profile_picture)
|
||||
)
|
||||
|
||||
user = cursor.fetchone()
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
|
||||
return dict(user) if user else None
|
||||
except Exception as e:
|
||||
print(f"Error creating user: {e}")
|
||||
if conn:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_or_create_user(google_id, email, name, profile_picture):
|
||||
"""Get existing user or create new one (upsert logic)."""
|
||||
user = get_user_by_google_id(google_id)
|
||||
|
||||
if user:
|
||||
return user
|
||||
else:
|
||||
return create_user(google_id, email, name, profile_picture)
|
||||
|
||||
|
||||
def get_files_by_user(user_id):
|
||||
"""Get all files for a user, ordered by upload date (newest first)."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor(row_factory=dict_row)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT * FROM files
|
||||
WHERE user_id = %s
|
||||
ORDER BY uploaded_at DESC
|
||||
""",
|
||||
(user_id,)
|
||||
)
|
||||
|
||||
files = cursor.fetchall()
|
||||
cursor.close()
|
||||
|
||||
return [dict(f) for f in files] if files else []
|
||||
except Exception as e:
|
||||
print(f"Error fetching files: {e}")
|
||||
return []
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def add_file(user_id, filename, original_name, s3_key, file_size, file_type):
|
||||
"""Add a new file record to the database."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor(row_factory=dict_row)
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO files (user_id, filename, original_name, s3_key, file_size, file_type)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
RETURNING *
|
||||
""",
|
||||
(user_id, filename, original_name, s3_key, file_size, file_type)
|
||||
)
|
||||
|
||||
file = cursor.fetchone()
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
|
||||
return dict(file) if file else None
|
||||
except Exception as e:
|
||||
print(f"Error adding file: {e}")
|
||||
if conn:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def delete_file(file_id, user_id):
|
||||
"""Delete a file record (with ownership verification)."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(
|
||||
"DELETE FROM files WHERE id = %s AND user_id = %s",
|
||||
(file_id, user_id)
|
||||
)
|
||||
|
||||
deleted_count = cursor.rowcount
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
|
||||
return deleted_count > 0
|
||||
except Exception as e:
|
||||
print(f"Error deleting file: {e}")
|
||||
if conn:
|
||||
conn.rollback()
|
||||
return False
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_file(file_id, user_id):
|
||||
"""Get a single file by ID (with ownership verification)."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor(row_factory=dict_row)
|
||||
|
||||
cursor.execute(
|
||||
"SELECT * FROM files WHERE id = %s AND user_id = %s",
|
||||
(file_id, user_id)
|
||||
)
|
||||
|
||||
file = cursor.fetchone()
|
||||
cursor.close()
|
||||
|
||||
return dict(file) if file else None
|
||||
except Exception as e:
|
||||
print(f"Error fetching file: {e}")
|
||||
return None
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def set_share_token(file_id, user_id, token):
|
||||
"""Set a share token for a file (with ownership verification)."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
UPDATE files
|
||||
SET share_token = %s
|
||||
WHERE id = %s AND user_id = %s
|
||||
""",
|
||||
(token, file_id, user_id)
|
||||
)
|
||||
|
||||
updated_count = cursor.rowcount
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
|
||||
return updated_count > 0
|
||||
except Exception as e:
|
||||
print(f"Error setting share token: {e}")
|
||||
if conn:
|
||||
conn.rollback()
|
||||
return False
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_file_by_share_token(token):
|
||||
"""Get a file by its share token (no user_id check for public access)."""
|
||||
conn = None
|
||||
try:
|
||||
conn = get_connection()
|
||||
cursor = conn.cursor(row_factory=dict_row)
|
||||
|
||||
cursor.execute(
|
||||
"SELECT * FROM files WHERE share_token = %s",
|
||||
(token,)
|
||||
)
|
||||
|
||||
file = cursor.fetchone()
|
||||
cursor.close()
|
||||
|
||||
return dict(file) if file else None
|
||||
except Exception as e:
|
||||
print(f"Error fetching file by share token: {e}")
|
||||
return None
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
258
mini-dropbox/CODE/app/files.py
Normal file
258
mini-dropbox/CODE/app/files.py
Normal file
@@ -0,0 +1,258 @@
|
||||
# === FILE: app/files.py ===
|
||||
import os
|
||||
import uuid
|
||||
import secrets
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, render_template, request, jsonify, session, redirect, url_for
|
||||
from werkzeug.utils import secure_filename
|
||||
from app.auth import login_required
|
||||
from app import db, s3
|
||||
|
||||
|
||||
files_bp = Blueprint('files', __name__, url_prefix='/files')
|
||||
|
||||
|
||||
# Maximum file size: 100MB
|
||||
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100 MB in bytes
|
||||
|
||||
|
||||
def format_file_size(size_bytes):
|
||||
"""Convert bytes to human-readable format."""
|
||||
if size_bytes < 1024:
|
||||
return f"{size_bytes} B"
|
||||
elif size_bytes < 1024 * 1024:
|
||||
return f"{size_bytes / 1024:.2f} KB"
|
||||
elif size_bytes < 1024 * 1024 * 1024:
|
||||
return f"{size_bytes / (1024 * 1024):.2f} MB"
|
||||
else:
|
||||
return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"
|
||||
|
||||
|
||||
def get_file_category(file_type):
|
||||
"""Determine file category based on MIME type."""
|
||||
if not file_type:
|
||||
return 'other'
|
||||
|
||||
file_type = file_type.lower()
|
||||
|
||||
if file_type.startswith('image/'):
|
||||
return 'image'
|
||||
elif file_type == 'application/pdf':
|
||||
return 'pdf'
|
||||
elif file_type.startswith('video/'):
|
||||
return 'video'
|
||||
elif file_type.startswith('audio/'):
|
||||
return 'audio'
|
||||
elif file_type in ['application/zip', 'application/x-zip-compressed', 'application/x-rar-compressed', 'application/x-7z-compressed']:
|
||||
return 'archive'
|
||||
elif file_type in ['application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'text/plain', 'text/csv']:
|
||||
return 'document'
|
||||
else:
|
||||
return 'other'
|
||||
|
||||
|
||||
def format_date(dt):
|
||||
"""Format datetime to readable string."""
|
||||
if not dt:
|
||||
return ''
|
||||
|
||||
if isinstance(dt, str):
|
||||
try:
|
||||
dt = datetime.fromisoformat(dt)
|
||||
except:
|
||||
return dt
|
||||
|
||||
return dt.strftime('%b %d, %Y')
|
||||
|
||||
|
||||
@files_bp.route('/')
|
||||
@login_required
|
||||
def dashboard():
|
||||
"""Display the main dashboard with user's files."""
|
||||
user_id = session['user']['id']
|
||||
|
||||
# Get all files for the user
|
||||
files = db.get_files_by_user(user_id)
|
||||
|
||||
# Enhance file data with formatted information
|
||||
for file in files:
|
||||
file['size_formatted'] = format_file_size(file['file_size'] or 0)
|
||||
file['category'] = get_file_category(file['file_type'])
|
||||
file['date_formatted'] = format_date(file['uploaded_at'])
|
||||
|
||||
return render_template(
|
||||
'dashboard.html',
|
||||
user=session['user'],
|
||||
files=files
|
||||
)
|
||||
|
||||
|
||||
@files_bp.route('/upload', methods=['POST'])
|
||||
@login_required
|
||||
def upload():
|
||||
"""Handle file upload."""
|
||||
try:
|
||||
# Check if file is present
|
||||
if 'file' not in request.files:
|
||||
return jsonify({'success': False, 'message': 'No file provided'}), 400
|
||||
|
||||
file = request.files['file']
|
||||
|
||||
if file.filename == '':
|
||||
return jsonify({'success': False, 'message': 'No file selected'}), 400
|
||||
|
||||
# Get file size
|
||||
file.seek(0, os.SEEK_END)
|
||||
file_size = file.tell()
|
||||
file.seek(0)
|
||||
|
||||
# Validate file size
|
||||
if file_size > MAX_FILE_SIZE:
|
||||
return jsonify({'success': False, 'message': 'File size exceeds 100MB limit'}), 400
|
||||
|
||||
# Get user ID
|
||||
user_id = session['user']['id']
|
||||
|
||||
# Secure the filename
|
||||
original_name = file.filename
|
||||
safe_filename = secure_filename(original_name)
|
||||
|
||||
# Generate unique S3 key
|
||||
unique_id = uuid.uuid4()
|
||||
s3_key = f"{user_id}/{unique_id}_{safe_filename}"
|
||||
|
||||
# Get content type
|
||||
content_type = file.content_type or 'application/octet-stream'
|
||||
|
||||
# Upload to S3
|
||||
s3.upload_file(file, s3_key, content_type)
|
||||
|
||||
# Save metadata to database
|
||||
db.add_file(
|
||||
user_id=user_id,
|
||||
filename=safe_filename,
|
||||
original_name=original_name,
|
||||
s3_key=s3_key,
|
||||
file_size=file_size,
|
||||
file_type=content_type
|
||||
)
|
||||
|
||||
return jsonify({'success': True, 'message': 'File uploaded successfully'}), 200
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error uploading file: {e}")
|
||||
return jsonify({'success': False, 'message': 'Failed to upload file'}), 500
|
||||
|
||||
|
||||
@files_bp.route('/download/<int:file_id>')
|
||||
@login_required
|
||||
def download(file_id):
|
||||
"""Generate presigned URL and redirect for file download."""
|
||||
try:
|
||||
user_id = session['user']['id']
|
||||
|
||||
# Get file from database (verifies ownership)
|
||||
file = db.get_file(file_id, user_id)
|
||||
|
||||
if not file:
|
||||
return jsonify({'success': False, 'message': 'File not found'}), 404
|
||||
|
||||
# Generate presigned URL (15 minutes)
|
||||
presigned_url = s3.generate_presigned_url(file['s3_key'], expiry=900)
|
||||
|
||||
# Redirect to presigned URL
|
||||
return redirect(presigned_url)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error downloading file: {e}")
|
||||
return jsonify({'success': False, 'message': 'Failed to download file'}), 500
|
||||
|
||||
|
||||
@files_bp.route('/delete/<int:file_id>', methods=['POST'])
|
||||
@login_required
|
||||
def delete(file_id):
|
||||
"""Delete a file."""
|
||||
try:
|
||||
user_id = session['user']['id']
|
||||
|
||||
# Get file from database (verifies ownership)
|
||||
file = db.get_file(file_id, user_id)
|
||||
|
||||
if not file:
|
||||
return jsonify({'success': False, 'message': 'File not found'}), 404
|
||||
|
||||
# Delete from S3
|
||||
s3.delete_file(file['s3_key'])
|
||||
|
||||
# Delete from database
|
||||
db.delete_file(file_id, user_id)
|
||||
|
||||
return jsonify({'success': True, 'message': 'File deleted successfully'}), 200
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error deleting file: {e}")
|
||||
return jsonify({'success': False, 'message': 'Failed to delete file'}), 500
|
||||
|
||||
|
||||
@files_bp.route('/share/<int:file_id>', methods=['POST'])
|
||||
@login_required
|
||||
def share(file_id):
|
||||
"""Generate a shareable link for a file."""
|
||||
try:
|
||||
user_id = session['user']['id']
|
||||
|
||||
# Get file from database (verifies ownership)
|
||||
file = db.get_file(file_id, user_id)
|
||||
|
||||
if not file:
|
||||
return jsonify({'success': False, 'message': 'File not found'}), 404
|
||||
|
||||
# Generate share token
|
||||
share_token = secrets.token_urlsafe(32)
|
||||
|
||||
# Save token to database
|
||||
db.set_share_token(file_id, user_id, share_token)
|
||||
|
||||
# Generate 7-day presigned URL
|
||||
share_url = s3.generate_share_url(file['s3_key'], expiry=604800)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'share_url': share_url
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error sharing file: {e}")
|
||||
return jsonify({'success': False, 'message': 'Failed to generate share link'}), 500
|
||||
|
||||
|
||||
@files_bp.route('/preview/<int:file_id>')
|
||||
@login_required
|
||||
def preview(file_id):
|
||||
"""Generate a preview URL for images and PDFs."""
|
||||
try:
|
||||
user_id = session['user']['id']
|
||||
|
||||
# Get file from database (verifies ownership)
|
||||
file = db.get_file(file_id, user_id)
|
||||
|
||||
if not file:
|
||||
return jsonify({'success': False, 'message': 'File not found'}), 404
|
||||
|
||||
# Check if file is previewable (image or PDF)
|
||||
file_type = file.get('file_type', '').lower()
|
||||
|
||||
if file_type.startswith('image/') or file_type == 'application/pdf':
|
||||
# Return CloudFront URL for preview
|
||||
preview_url = s3.get_cloudfront_url(file['s3_key'])
|
||||
return jsonify({'success': True, 'preview_url': preview_url}), 200
|
||||
else:
|
||||
# Not previewable, redirect to download
|
||||
return redirect(url_for('files.download', file_id=file_id))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error generating preview: {e}")
|
||||
return jsonify({'success': False, 'message': 'Failed to generate preview'}), 500
|
||||
123
mini-dropbox/CODE/app/s3.py
Normal file
123
mini-dropbox/CODE/app/s3.py
Normal file
@@ -0,0 +1,123 @@
|
||||
# === FILE: app/s3.py ===
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
from app.config import Config
|
||||
|
||||
|
||||
def get_s3_client():
|
||||
"""Create and return an S3 client using IAM role credentials."""
|
||||
return boto3.client('s3', region_name=Config.AWS_REGION)
|
||||
|
||||
|
||||
def upload_file(file_obj, s3_key, content_type):
|
||||
"""
|
||||
Upload a file to S3.
|
||||
|
||||
Args:
|
||||
file_obj: File object to upload
|
||||
s3_key: S3 key (path) for the file
|
||||
content_type: MIME type of the file
|
||||
|
||||
Returns:
|
||||
True if successful, raises exception otherwise
|
||||
"""
|
||||
try:
|
||||
s3_client = get_s3_client()
|
||||
|
||||
s3_client.upload_fileobj(
|
||||
file_obj,
|
||||
Config.S3_BUCKET_NAME,
|
||||
s3_key,
|
||||
ExtraArgs={
|
||||
'ContentType': content_type
|
||||
}
|
||||
)
|
||||
|
||||
return True
|
||||
except ClientError as e:
|
||||
print(f"Error uploading file to S3: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def delete_file(s3_key):
|
||||
"""
|
||||
Delete a file from S3.
|
||||
|
||||
Args:
|
||||
s3_key: S3 key (path) of the file to delete
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
s3_client = get_s3_client()
|
||||
|
||||
s3_client.delete_object(
|
||||
Bucket=Config.S3_BUCKET_NAME,
|
||||
Key=s3_key
|
||||
)
|
||||
|
||||
return True
|
||||
except ClientError as e:
|
||||
print(f"Error deleting file from S3: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def generate_presigned_url(s3_key, expiry=900):
|
||||
"""
|
||||
Generate a presigned URL for downloading a file.
|
||||
|
||||
Args:
|
||||
s3_key: S3 key (path) of the file
|
||||
expiry: Expiration time in seconds (default 15 minutes)
|
||||
|
||||
Returns:
|
||||
Presigned URL string
|
||||
"""
|
||||
try:
|
||||
s3_client = get_s3_client()
|
||||
|
||||
url = s3_client.generate_presigned_url(
|
||||
'get_object',
|
||||
Params={
|
||||
'Bucket': Config.S3_BUCKET_NAME,
|
||||
'Key': s3_key
|
||||
},
|
||||
ExpiresIn=expiry
|
||||
)
|
||||
|
||||
return url
|
||||
except ClientError as e:
|
||||
print(f"Error generating presigned URL: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def generate_share_url(s3_key, expiry=604800):
|
||||
"""
|
||||
Generate a presigned URL for sharing a file (valid for 7 days).
|
||||
|
||||
Args:
|
||||
s3_key: S3 key (path) of the file
|
||||
expiry: Expiration time in seconds (default 7 days)
|
||||
|
||||
Returns:
|
||||
Presigned URL string
|
||||
"""
|
||||
return generate_presigned_url(s3_key, expiry)
|
||||
|
||||
|
||||
def get_cloudfront_url(s3_key):
|
||||
"""
|
||||
Generate a CloudFront URL for a file (for preview purposes).
|
||||
|
||||
Args:
|
||||
s3_key: S3 key (path) of the file
|
||||
|
||||
Returns:
|
||||
CloudFront URL string
|
||||
"""
|
||||
if Config.CLOUDFRONT_DOMAIN:
|
||||
return f"https://{Config.CLOUDFRONT_DOMAIN}/{s3_key}"
|
||||
else:
|
||||
# Fallback to S3 URL if CloudFront is not configured
|
||||
return f"https://{Config.S3_BUCKET_NAME}.s3.{Config.AWS_REGION}.amazonaws.com/{s3_key}"
|
||||
226
mini-dropbox/CODE/app/static/css/style.css
Normal file
226
mini-dropbox/CODE/app/static/css/style.css
Normal file
@@ -0,0 +1,226 @@
|
||||
/* === FILE: app/static/css/style.css === */
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #0f1117;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(180deg, #6366f1, #8b5cf6);
|
||||
border-radius: 10px;
|
||||
border: 2px solid #0f1117;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(180deg, #4f46e5, #7c3aed);
|
||||
}
|
||||
|
||||
/* Drop zone states */
|
||||
#drop-zone {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
#drop-zone.drag-over {
|
||||
border-color: #6366f1 !important;
|
||||
background-color: rgba(99, 102, 241, 0.1) !important;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
/* File card hover effect */
|
||||
.file-card {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.file-card:hover {
|
||||
transform: translateY(-4px) scale(1.02);
|
||||
}
|
||||
|
||||
/* Progress bar animation */
|
||||
#progress-bar {
|
||||
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#progress-bar::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent,
|
||||
rgba(255, 255, 255, 0.3),
|
||||
transparent
|
||||
);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
/* Button loading spinner */
|
||||
.btn-loading {
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.btn-loading::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-left: -10px;
|
||||
margin-top: -10px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #ffffff;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal animations */
|
||||
.modal-enter {
|
||||
animation: modalFadeIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@keyframes modalFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-exit {
|
||||
animation: modalFadeOut 0.2s cubic-bezier(0.4, 0, 1, 1);
|
||||
}
|
||||
|
||||
@keyframes modalFadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(20px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Smooth transitions */
|
||||
button, a, .action-btn {
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Action button styles */
|
||||
.action-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Flash message animation */
|
||||
.flash-message {
|
||||
animation: slideInRight 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Pulse animation for empty state */
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
/* Gradient text animation */
|
||||
@keyframes gradient {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.bg-clip-text {
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Glassmorphism effect */
|
||||
.glass {
|
||||
background: rgba(26, 29, 39, 0.8);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Hover glow effect */
|
||||
.hover-glow {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hover-glow::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 2px;
|
||||
background: linear-gradient(45deg, #6366f1, #8b5cf6, #ec4899);
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.hover-glow:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
398
mini-dropbox/CODE/app/static/js/dashboard.js
Normal file
398
mini-dropbox/CODE/app/static/js/dashboard.js
Normal file
@@ -0,0 +1,398 @@
|
||||
// === FILE: app/static/js/dashboard.js ===
|
||||
|
||||
// Global variables
|
||||
let selectedFile = null;
|
||||
let deleteFileId = null;
|
||||
|
||||
// ========================================
|
||||
// UPLOAD FUNCTIONALITY
|
||||
// ========================================
|
||||
|
||||
// Open upload modal
|
||||
document.getElementById('open-upload-modal').addEventListener('click', function() {
|
||||
const modal = document.getElementById('upload-modal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex', 'modal-enter');
|
||||
resetUploadModal();
|
||||
});
|
||||
|
||||
// Close upload modal
|
||||
document.getElementById('cancel-upload').addEventListener('click', function() {
|
||||
closeUploadModal();
|
||||
});
|
||||
|
||||
// Close upload modal with X button
|
||||
document.getElementById('close-upload-modal').addEventListener('click', function() {
|
||||
closeUploadModal();
|
||||
});
|
||||
|
||||
function closeUploadModal() {
|
||||
const modal = document.getElementById('upload-modal');
|
||||
modal.classList.add('modal-exit');
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex', 'modal-enter', 'modal-exit');
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// Reset upload modal
|
||||
function resetUploadModal() {
|
||||
selectedFile = null;
|
||||
document.getElementById('file-input').value = '';
|
||||
document.getElementById('selected-file').classList.add('hidden');
|
||||
document.getElementById('progress-container').classList.add('hidden');
|
||||
document.getElementById('upload-error').classList.add('hidden');
|
||||
document.getElementById('progress-bar').style.width = '0%';
|
||||
document.getElementById('upload-btn').disabled = true;
|
||||
}
|
||||
|
||||
// Drop zone click
|
||||
document.getElementById('drop-zone').addEventListener('click', function() {
|
||||
document.getElementById('file-input').click();
|
||||
});
|
||||
|
||||
// File input change
|
||||
document.getElementById('file-input').addEventListener('change', function(e) {
|
||||
if (e.target.files.length > 0) {
|
||||
handleFileSelect(e.target.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Drag and drop handlers
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
|
||||
dropZone.addEventListener('dragover', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dropZone.classList.add('drag-over');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dropZone.classList.remove('drag-over');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dropZone.classList.remove('drag-over');
|
||||
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
handleFileSelect(e.dataTransfer.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle file selection
|
||||
function handleFileSelect(file) {
|
||||
// Check file size (100MB max)
|
||||
const maxSize = 100 * 1024 * 1024; // 100MB
|
||||
if (file.size > maxSize) {
|
||||
showUploadError('File size exceeds 100MB limit');
|
||||
return;
|
||||
}
|
||||
|
||||
selectedFile = file;
|
||||
document.getElementById('selected-file-name').textContent = file.name;
|
||||
document.getElementById('selected-file').classList.remove('hidden');
|
||||
document.getElementById('upload-btn').disabled = false;
|
||||
document.getElementById('upload-error').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Show upload error
|
||||
function showUploadError(message) {
|
||||
const errorDiv = document.getElementById('upload-error');
|
||||
const errorText = document.getElementById('upload-error-text');
|
||||
if (errorText) {
|
||||
errorText.textContent = message;
|
||||
} else {
|
||||
errorDiv.textContent = message;
|
||||
}
|
||||
errorDiv.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Upload button click
|
||||
document.getElementById('upload-btn').addEventListener('click', function() {
|
||||
if (!selectedFile) {
|
||||
showUploadError('Please select a file');
|
||||
return;
|
||||
}
|
||||
|
||||
uploadFile(selectedFile);
|
||||
});
|
||||
|
||||
// Upload file function
|
||||
function uploadFile(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
// Show progress bar
|
||||
document.getElementById('progress-container').classList.remove('hidden');
|
||||
document.getElementById('upload-btn').disabled = true;
|
||||
document.getElementById('cancel-upload').disabled = true;
|
||||
|
||||
// Use XMLHttpRequest for progress tracking
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
// Progress handler
|
||||
xhr.upload.addEventListener('progress', function(e) {
|
||||
if (e.lengthComputable) {
|
||||
const percentage = (e.loaded / e.total) * 100;
|
||||
const roundedPercent = Math.round(percentage);
|
||||
document.getElementById('progress-bar').style.width = percentage + '%';
|
||||
document.getElementById('progress-text').textContent = `Uploading...`;
|
||||
const progressPercent = document.getElementById('progress-percent');
|
||||
if (progressPercent) {
|
||||
progressPercent.textContent = roundedPercent + '%';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Load handler (upload complete)
|
||||
xhr.addEventListener('load', function() {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
const response = JSON.parse(xhr.responseText);
|
||||
if (response.success) {
|
||||
// Success - reload page to show new file
|
||||
window.location.reload();
|
||||
} else {
|
||||
showUploadError(response.message || 'Upload failed');
|
||||
document.getElementById('upload-btn').disabled = false;
|
||||
document.getElementById('cancel-upload').disabled = false;
|
||||
}
|
||||
} catch (e) {
|
||||
showUploadError('Upload failed');
|
||||
document.getElementById('upload-btn').disabled = false;
|
||||
document.getElementById('cancel-upload').disabled = false;
|
||||
}
|
||||
} else {
|
||||
showUploadError('Upload failed. Please try again.');
|
||||
document.getElementById('upload-btn').disabled = false;
|
||||
document.getElementById('cancel-upload').disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Error handler
|
||||
xhr.addEventListener('error', function() {
|
||||
showUploadError('Network error. Please try again.');
|
||||
document.getElementById('upload-btn').disabled = false;
|
||||
document.getElementById('cancel-upload').disabled = false;
|
||||
});
|
||||
|
||||
// Send request
|
||||
xhr.open('POST', '/files/upload');
|
||||
xhr.send(formData);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// DELETE FUNCTIONALITY
|
||||
// ========================================
|
||||
|
||||
function confirmDelete(fileId, fileName) {
|
||||
deleteFileId = fileId;
|
||||
document.getElementById('delete-file-name').textContent = fileName;
|
||||
|
||||
const modal = document.getElementById('delete-modal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex', 'modal-enter');
|
||||
}
|
||||
|
||||
// Cancel delete
|
||||
document.getElementById('cancel-delete').addEventListener('click', function() {
|
||||
closeDeleteModal();
|
||||
});
|
||||
|
||||
function closeDeleteModal() {
|
||||
const modal = document.getElementById('delete-modal');
|
||||
modal.classList.add('modal-exit');
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex', 'modal-enter', 'modal-exit');
|
||||
deleteFileId = null;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// Confirm delete
|
||||
document.getElementById('confirm-delete-btn').addEventListener('click', function() {
|
||||
if (!deleteFileId) return;
|
||||
|
||||
const btn = this;
|
||||
btn.classList.add('btn-loading');
|
||||
btn.disabled = true;
|
||||
|
||||
fetch(`/files/delete/${deleteFileId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Remove file card from DOM
|
||||
const fileCard = document.querySelector(`.file-card[data-file-id="${deleteFileId}"]`);
|
||||
if (fileCard) {
|
||||
fileCard.style.opacity = '0';
|
||||
fileCard.style.transform = 'scale(0.9)';
|
||||
setTimeout(() => {
|
||||
fileCard.remove();
|
||||
|
||||
// Check if no files left
|
||||
const filesGrid = document.getElementById('files-grid');
|
||||
if (filesGrid && filesGrid.children.length === 0) {
|
||||
window.location.reload();
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
closeDeleteModal();
|
||||
showFlashMessage('File deleted successfully', 'success');
|
||||
} else {
|
||||
showFlashMessage(data.message || 'Failed to delete file', 'error');
|
||||
btn.classList.remove('btn-loading');
|
||||
btn.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showFlashMessage('Failed to delete file', 'error');
|
||||
btn.classList.remove('btn-loading');
|
||||
btn.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// SHARE FUNCTIONALITY
|
||||
// ========================================
|
||||
|
||||
function shareFile(fileId) {
|
||||
fetch(`/files/share/${fileId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('share-url').value = data.share_url;
|
||||
|
||||
const modal = document.getElementById('share-modal');
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex', 'modal-enter');
|
||||
} else {
|
||||
showFlashMessage(data.message || 'Failed to generate share link', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showFlashMessage('Failed to generate share link', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Close share modal
|
||||
document.getElementById('close-share-modal').addEventListener('click', function() {
|
||||
closeShareModal();
|
||||
});
|
||||
|
||||
function closeShareModal() {
|
||||
const modal = document.getElementById('share-modal');
|
||||
modal.classList.add('modal-exit');
|
||||
setTimeout(() => {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex', 'modal-enter', 'modal-exit');
|
||||
document.getElementById('copy-feedback').classList.add('hidden');
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// Copy link button
|
||||
document.getElementById('copy-link-btn').addEventListener('click', function() {
|
||||
const shareUrl = document.getElementById('share-url');
|
||||
shareUrl.select();
|
||||
shareUrl.setSelectionRange(0, 99999); // For mobile devices
|
||||
|
||||
// Copy to clipboard
|
||||
navigator.clipboard.writeText(shareUrl.value)
|
||||
.then(() => {
|
||||
const feedback = document.getElementById('copy-feedback');
|
||||
feedback.classList.remove('hidden');
|
||||
|
||||
// Hide after 2 seconds
|
||||
setTimeout(() => {
|
||||
feedback.classList.add('hidden');
|
||||
}, 2000);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
showFlashMessage('Failed to copy link', 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// PREVIEW FUNCTIONALITY
|
||||
// ========================================
|
||||
|
||||
function previewFile(fileId) {
|
||||
fetch(`/files/preview/${fileId}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success && data.preview_url) {
|
||||
window.open(data.preview_url, '_blank');
|
||||
} else {
|
||||
showFlashMessage('Preview not available', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
showFlashMessage('Failed to generate preview', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// HELPER FUNCTIONS
|
||||
// ========================================
|
||||
|
||||
function showFlashMessage(message, category) {
|
||||
const flashContainer = document.getElementById('flash-messages') || createFlashContainer();
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `flash-message px-6 py-3 rounded-lg shadow-lg text-white`;
|
||||
|
||||
if (category === 'success') {
|
||||
messageDiv.classList.add('bg-[#22c55e]');
|
||||
} else if (category === 'error') {
|
||||
messageDiv.classList.add('bg-[#ef4444]');
|
||||
} else {
|
||||
messageDiv.classList.add('bg-[#6366f1]');
|
||||
}
|
||||
|
||||
messageDiv.textContent = message;
|
||||
flashContainer.appendChild(messageDiv);
|
||||
|
||||
// Auto-dismiss after 4 seconds
|
||||
setTimeout(() => {
|
||||
messageDiv.style.opacity = '0';
|
||||
messageDiv.style.transform = 'translateX(100%)';
|
||||
messageDiv.style.transition = 'all 0.3s ease-out';
|
||||
setTimeout(() => {
|
||||
messageDiv.remove();
|
||||
}, 300);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
function createFlashContainer() {
|
||||
const container = document.createElement('div');
|
||||
container.id = 'flash-messages';
|
||||
container.className = 'fixed top-4 right-4 z-50 space-y-2';
|
||||
document.body.appendChild(container);
|
||||
return container;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// INITIALIZATION
|
||||
// ========================================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Dashboard loaded');
|
||||
});
|
||||
97
mini-dropbox/CODE/app/templates/base.html
Normal file
97
mini-dropbox/CODE/app/templates/base.html
Normal file
@@ -0,0 +1,97 @@
|
||||
<!-- === FILE: app/templates/base.html === -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Cloud Drive{% endblock %}</title>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
background-color: #0f1117;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen">
|
||||
<!-- Navbar -->
|
||||
{% if session.user %}
|
||||
<nav class="bg-[#1a1d27] border-b border-[#2a2d3e]">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<!-- Left: Logo and Brand -->
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg class="w-8 h-8 text-[#6366f1]" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M5.5 16a3.5 3.5 0 01-.369-6.98 4 4 0 117.753-1.977A4.5 4.5 0 1113.5 16h-8z"/>
|
||||
</svg>
|
||||
<span class="text-xl font-bold text-white">Cloud Drive</span>
|
||||
</div>
|
||||
|
||||
<!-- Right: User info and logout -->
|
||||
<div class="flex items-center space-x-4">
|
||||
<img src="{{ session.user.profile_picture }}" alt="{{ session.user.name }}" class="w-8 h-8 rounded-full border-2 border-[#6366f1]">
|
||||
<span class="text-sm text-[#94a3b8]">{{ session.user.name }}</span>
|
||||
<a href="{{ url_for('auth.logout') }}" class="text-sm text-[#ef4444] hover:text-[#dc2626] transition-colors">
|
||||
Sign out
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
<!-- Flash Messages -->
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<div id="flash-messages" class="fixed top-4 right-4 z-50 space-y-2">
|
||||
{% for category, message in messages %}
|
||||
<div class="flash-message px-6 py-3 rounded-lg shadow-lg {% if category == 'success' %}bg-[#22c55e]{% elif category == 'error' %}bg-[#ef4444]{% else %}bg-[#6366f1]{% endif %} text-white">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<!-- Main Content -->
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script>
|
||||
// Auto-dismiss flash messages after 4 seconds
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const flashMessages = document.querySelectorAll('.flash-message');
|
||||
flashMessages.forEach(function(message) {
|
||||
setTimeout(function() {
|
||||
message.style.opacity = '0';
|
||||
message.style.transform = 'translateX(100%)';
|
||||
message.style.transition = 'all 0.3s ease-out';
|
||||
setTimeout(function() {
|
||||
message.remove();
|
||||
}, 300);
|
||||
}, 4000);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
346
mini-dropbox/CODE/app/templates/dashboard.html
Normal file
346
mini-dropbox/CODE/app/templates/dashboard.html
Normal file
@@ -0,0 +1,346 @@
|
||||
<!-- === FILE: app/templates/dashboard.html === -->
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - Cloud Drive{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
|
||||
<!-- Top Section with Stats Cards -->
|
||||
<div class="mb-10">
|
||||
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6 mb-8">
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold text-white mb-3 bg-gradient-to-r from-white to-[#94a3b8] bg-clip-text text-transparent">
|
||||
Hello, {{ user.name }} 👋
|
||||
</h1>
|
||||
<p class="text-[#64748b] text-lg">Manage your files in the cloud</p>
|
||||
</div>
|
||||
<button id="open-upload-modal" class="bg-gradient-to-r from-[#6366f1] to-[#8b5cf6] hover:from-[#4f46e5] hover:to-[#7c3aed] text-white font-semibold px-8 py-4 rounded-xl transition-all duration-300 shadow-xl shadow-[#6366f1]/20 hover:shadow-2xl hover:shadow-[#6366f1]/30 transform hover:scale-105 flex items-center space-x-3 group">
|
||||
<svg class="w-6 h-6 group-hover:rotate-180 transition-transform duration-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
|
||||
</svg>
|
||||
<span>Upload File</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div class="bg-gradient-to-br from-[#1a1d27] to-[#0f1117] rounded-2xl p-6 border border-[#2a2d3e] hover:border-[#6366f1] transition-all duration-300 shadow-lg hover:shadow-[#6366f1]/10">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="p-3 bg-[#6366f1]/10 rounded-xl">
|
||||
<svg class="w-8 h-8 text-[#6366f1]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-3xl font-bold text-white">{{ files|length }}</p>
|
||||
<p class="text-[#64748b] text-sm">Total Files</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-[#1a1d27] to-[#0f1117] rounded-2xl p-6 border border-[#2a2d3e] hover:border-[#8b5cf6] transition-all duration-300 shadow-lg hover:shadow-[#8b5cf6]/10">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="p-3 bg-[#8b5cf6]/10 rounded-xl">
|
||||
<svg class="w-8 h-8 text-[#8b5cf6]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-3xl font-bold text-white">∞</p>
|
||||
<p class="text-[#64748b] text-sm">Storage Available</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-[#1a1d27] to-[#0f1117] rounded-2xl p-6 border border-[#2a2d3e] hover:border-[#22c55e] transition-all duration-300 shadow-lg hover:shadow-[#22c55e]/10">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="p-3 bg-[#22c55e]/10 rounded-xl">
|
||||
<svg class="w-8 h-8 text-[#22c55e]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-3xl font-bold text-white">100%</p>
|
||||
<p class="text-[#64748b] text-sm">Secure Storage</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Files Grid -->
|
||||
{% if files %}
|
||||
<div class="mb-6">
|
||||
<h2 class="text-2xl font-bold text-white mb-6 flex items-center">
|
||||
<svg class="w-6 h-6 mr-3 text-[#6366f1]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"/>
|
||||
</svg>
|
||||
Your Files
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div id="files-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{% for file in files %}
|
||||
<div class="file-card group bg-gradient-to-br from-[#1a1d27] to-[#14161f] rounded-2xl p-6 border border-[#2a2d3e] hover:border-[#6366f1] transition-all duration-300 transform hover:scale-105 hover:shadow-2xl hover:shadow-[#6366f1]/20 cursor-pointer" data-file-id="{{ file.id }}">
|
||||
<!-- File Icon and Name -->
|
||||
<div class="mb-6">
|
||||
<div class="flex items-center justify-center w-16 h-16 mb-4 rounded-2xl bg-gradient-to-br {% if file.category == 'image' %}from-[#ec4899] to-[#f43f5e]{% elif file.category == 'pdf' %}from-[#ef4444] to-[#dc2626]{% elif file.category == 'video' %}from-[#8b5cf6] to-[#7c3aed]{% elif file.category == 'audio' %}from-[#6366f1] to-[#4f46e5]{% elif file.category == 'archive' %}from-[#f59e0b] to-[#d97706]{% elif file.category == 'document' %}from-[#3b82f6] to-[#2563eb]{% else %}from-[#64748b] to-[#475569]{% endif %} shadow-lg group-hover:shadow-xl transition-all duration-300">
|
||||
<span class="text-4xl">
|
||||
{% if file.category == 'image' %}🖼️
|
||||
{% elif file.category == 'pdf' %}📄
|
||||
{% elif file.category == 'video' %}🎬
|
||||
{% elif file.category == 'audio' %}🎵
|
||||
{% elif file.category == 'archive' %}📦
|
||||
{% elif file.category == 'document' %}📝
|
||||
{% else %}📎
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-white font-semibold text-lg mb-2 truncate group-hover:text-[#6366f1] transition-colors" title="{{ file.original_name }}">
|
||||
{{ file.original_name }}
|
||||
</p>
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-[#64748b]">{{ file.size_formatted }}</span>
|
||||
<span class="text-[#475569]">{{ file.date_formatted }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="space-y-2">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<a href="{{ url_for('files.download', file_id=file.id) }}" class="action-btn flex items-center justify-center bg-[#6366f1] hover:bg-[#4f46e5] text-white text-sm font-medium px-3 py-2.5 rounded-lg transition-all duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-0.5">
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/>
|
||||
</svg>
|
||||
Download
|
||||
</a>
|
||||
<button onclick="shareFile({{ file.id }})" class="action-btn flex items-center justify-center bg-[#22c55e] hover:bg-[#16a34a] text-white text-sm font-medium px-3 py-2.5 rounded-lg transition-all duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-0.5">
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"/>
|
||||
</svg>
|
||||
Share
|
||||
</button>
|
||||
</div>
|
||||
<div class="grid {% if file.category in ['image', 'pdf'] %}grid-cols-2{% else %}grid-cols-1{% endif %} gap-2">
|
||||
{% if file.category in ['image', 'pdf'] %}
|
||||
<button onclick="previewFile({{ file.id }})" class="action-btn flex items-center justify-center bg-[#8b5cf6] hover:bg-[#7c3aed] text-white text-sm font-medium px-3 py-2.5 rounded-lg transition-all duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-0.5">
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
Preview
|
||||
</button>
|
||||
{% endif %}
|
||||
<button onclick="confirmDelete({{ file.id }}, '{{ file.original_name|replace("'", "\\'") }}')" class="action-btn flex items-center justify-center bg-[#ef4444] hover:bg-[#dc2626] text-white text-sm font-medium px-3 py-2.5 rounded-lg transition-all duration-200 shadow-md hover:shadow-lg transform hover:-translate-y-0.5">
|
||||
<svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Empty State -->
|
||||
<div class="text-center py-20">
|
||||
<div class="mb-8 relative">
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div class="w-32 h-32 bg-[#6366f1] opacity-20 rounded-full blur-3xl animate-pulse"></div>
|
||||
</div>
|
||||
<svg class="w-32 h-32 mx-auto text-[#475569] relative z-10" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M5.5 16a3.5 3.5 0 01-.369-6.98 4 4 0 117.753-1.977A4.5 4.5 0 1113.5 16h-8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-3xl font-bold text-white mb-3">No files yet</h2>
|
||||
<p class="text-[#64748b] text-lg mb-8 max-w-md mx-auto">Start uploading your files to the cloud. Your data is encrypted and secure.</p>
|
||||
<button onclick="document.getElementById('open-upload-modal').click()" class="bg-gradient-to-r from-[#6366f1] to-[#8b5cf6] hover:from-[#4f46e5] hover:to-[#7c3aed] text-white font-semibold px-8 py-4 rounded-xl transition-all duration-300 shadow-xl shadow-[#6366f1]/20 hover:shadow-2xl hover:shadow-[#6366f1]/30 transform hover:scale-105 inline-flex items-center space-x-2">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
|
||||
</svg>
|
||||
<span>Upload Your First File</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Upload Modal -->
|
||||
<div id="upload-modal" class="fixed inset-0 bg-black/80 backdrop-blur-md hidden items-center justify-center z-50 p-4">
|
||||
<div class="bg-gradient-to-br from-[#1a1d27] via-[#1a1d27] to-[#0f1117] rounded-3xl p-8 max-w-lg w-full mx-4 border border-[#2a2d3e] shadow-2xl transform transition-all duration-300">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-3xl font-bold text-white flex items-center">
|
||||
<svg class="w-8 h-8 mr-3 text-[#6366f1]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
|
||||
</svg>
|
||||
Upload File
|
||||
</h2>
|
||||
<button id="close-upload-modal" class="text-[#64748b] hover:text-white transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Drop Zone -->
|
||||
<div id="drop-zone" class="border-2 border-dashed border-[#2a2d3e] hover:border-[#6366f1] rounded-2xl p-12 text-center cursor-pointer transition-all duration-300 mb-6 bg-[#0f1117]/50 hover:bg-[#6366f1]/5 group">
|
||||
<div class="relative">
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div class="w-24 h-24 bg-[#6366f1] opacity-0 group-hover:opacity-20 rounded-full blur-2xl transition-opacity duration-300"></div>
|
||||
</div>
|
||||
<svg class="w-16 h-16 mx-auto text-[#64748b] group-hover:text-[#6366f1] mb-4 relative z-10 transition-all duration-300 group-hover:scale-110" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-white font-semibold text-lg mb-2">Drag & drop file here</p>
|
||||
<p class="text-[#64748b] mb-3">or click to browse</p>
|
||||
<p class="text-[#475569] text-sm">Maximum file size: 100MB</p>
|
||||
<input type="file" id="file-input" class="hidden">
|
||||
</div>
|
||||
|
||||
<!-- Selected File Display -->
|
||||
<div id="selected-file" class="hidden mb-6 p-4 bg-[#6366f1]/10 border border-[#6366f1]/30 rounded-xl">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="p-2 bg-[#6366f1] rounded-lg">
|
||||
<svg class="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<p class="text-white font-medium flex-1 truncate" id="selected-file-name"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Progress Bar -->
|
||||
<div id="progress-container" class="hidden mb-6">
|
||||
<div class="w-full bg-[#2a2d3e] rounded-full h-3 overflow-hidden shadow-inner">
|
||||
<div id="progress-bar" class="bg-gradient-to-r from-[#6366f1] to-[#8b5cf6] h-3 rounded-full transition-all duration-300 shadow-lg" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mt-3">
|
||||
<p class="text-[#64748b] text-sm" id="progress-text">Uploading...</p>
|
||||
<p class="text-[#6366f1] text-sm font-semibold" id="progress-percent">0%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
<div id="upload-error" class="hidden mb-6 p-4 bg-[#ef4444]/10 border border-[#ef4444] rounded-xl">
|
||||
<div class="flex items-start space-x-3">
|
||||
<svg class="w-5 h-5 text-[#ef4444] flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<p class="text-[#ef4444] text-sm flex-1" id="upload-error-text"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="flex space-x-3">
|
||||
<button id="upload-btn" class="flex-1 bg-gradient-to-r from-[#6366f1] to-[#8b5cf6] hover:from-[#4f46e5] hover:to-[#7c3aed] text-white font-semibold py-4 rounded-xl transition-all duration-300 shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-lg flex items-center justify-center space-x-2" disabled>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
|
||||
</svg>
|
||||
<span>Upload</span>
|
||||
</button>
|
||||
<button id="cancel-upload" class="flex-1 bg-[#2a2d3e] hover:bg-[#3a3d4e] text-white font-semibold py-4 rounded-xl transition-all duration-300">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Share Modal -->
|
||||
<div id="share-modal" class="fixed inset-0 bg-black/80 backdrop-blur-md hidden items-center justify-center z-50 p-4">
|
||||
<div class="bg-gradient-to-br from-[#1a1d27] via-[#1a1d27] to-[#0f1117] rounded-3xl p-8 max-w-md w-full mx-4 border border-[#2a2d3e] shadow-2xl transform transition-all duration-300">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-3xl font-bold text-white flex items-center">
|
||||
<svg class="w-8 h-8 mr-3 text-[#22c55e]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z"/>
|
||||
</svg>
|
||||
Share File
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="text-[#64748b] text-sm font-medium mb-2 block">Share Link</label>
|
||||
<div class="relative">
|
||||
<input type="text" id="share-url" readonly class="w-full bg-[#0f1117] border border-[#2a2d3e] text-white px-4 py-4 pr-12 rounded-xl focus:outline-none focus:border-[#22c55e] transition-colors font-mono text-sm">
|
||||
<button onclick="document.getElementById('copy-link-btn').click()" class="absolute right-2 top-1/2 -translate-y-1/2 p-2 hover:bg-[#2a2d3e] rounded-lg transition-colors">
|
||||
<svg class="w-5 h-5 text-[#64748b] hover:text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2 mb-6 p-4 bg-[#22c55e]/10 border border-[#22c55e]/30 rounded-xl">
|
||||
<svg class="w-5 h-5 text-[#22c55e] flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<p class="text-[#22c55e] text-sm">Valid for 7 days</p>
|
||||
</div>
|
||||
|
||||
<div id="copy-feedback" class="hidden mb-6 p-4 bg-[#22c55e]/10 border border-[#22c55e] rounded-xl">
|
||||
<div class="flex items-center space-x-3">
|
||||
<svg class="w-5 h-5 text-[#22c55e]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<p class="text-[#22c55e] text-sm font-medium">Link copied to clipboard!</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-3">
|
||||
<button id="copy-link-btn" class="flex-1 bg-gradient-to-r from-[#22c55e] to-[#16a34a] hover:from-[#16a34a] hover:to-[#15803d] text-white font-semibold py-4 rounded-xl transition-all duration-300 shadow-lg hover:shadow-xl flex items-center justify-center space-x-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
<span>Copy Link</span>
|
||||
</button>
|
||||
<button id="close-share-modal" class="flex-1 bg-[#2a2d3e] hover:bg-[#3a3d4e] text-white font-semibold py-4 rounded-xl transition-all duration-300">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirm Modal -->
|
||||
<div id="delete-modal" class="fixed inset-0 bg-black/80 backdrop-blur-md hidden items-center justify-center z-50 p-4">
|
||||
<div class="bg-gradient-to-br from-[#1a1d27] via-[#1a1d27] to-[#0f1117] rounded-3xl p-8 max-w-md w-full mx-4 border border-[#2a2d3e] shadow-2xl transform transition-all duration-300">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-3xl font-bold text-white flex items-center">
|
||||
<svg class="w-8 h-8 mr-3 text-[#ef4444]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
Delete File
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="mb-6 p-4 bg-[#ef4444]/10 border border-[#ef4444]/30 rounded-xl">
|
||||
<p class="text-[#94a3b8] leading-relaxed">
|
||||
Are you sure you want to delete <span id="delete-file-name" class="text-white font-semibold"></span>?
|
||||
</p>
|
||||
<p class="text-[#ef4444] text-sm mt-2 flex items-center">
|
||||
<svg class="w-4 h-4 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||||
</svg>
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-3">
|
||||
<button id="confirm-delete-btn" class="flex-1 bg-gradient-to-r from-[#ef4444] to-[#dc2626] hover:from-[#dc2626] hover:to-[#b91c1c] text-white font-semibold py-4 rounded-xl transition-all duration-300 shadow-lg hover:shadow-xl flex items-center justify-center space-x-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
<span>Delete Forever</span>
|
||||
</button>
|
||||
<button id="cancel-delete" class="flex-1 bg-[#2a2d3e] hover:bg-[#3a3d4e] text-white font-semibold py-4 rounded-xl transition-all duration-300">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
|
||||
{% endblock %}
|
||||
80
mini-dropbox/CODE/app/templates/login.html
Normal file
80
mini-dropbox/CODE/app/templates/login.html
Normal file
@@ -0,0 +1,80 @@
|
||||
<!-- === FILE: app/templates/login.html === -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cloud Drive - Sign In</title>
|
||||
|
||||
<!-- Tailwind CSS -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
background: linear-gradient(135deg, #0f1117 0%, #1a1d27 100%);
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
}
|
||||
|
||||
.glow {
|
||||
box-shadow: 0 0 30px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen flex items-center justify-center p-4">
|
||||
<div class="max-w-md w-full">
|
||||
<!-- Card -->
|
||||
<div class="bg-[#1a1d27] rounded-2xl p-8 shadow-2xl border border-[#2a2d3e] glow">
|
||||
<!-- Logo and Title -->
|
||||
<div class="text-center mb-8">
|
||||
<div class="flex justify-center mb-4">
|
||||
<svg class="w-16 h-16 text-[#6366f1]" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M5.5 16a3.5 3.5 0 01-.369-6.98 4 4 0 117.753-1.977A4.5 4.5 0 1113.5 16h-8z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">Cloud Drive</h1>
|
||||
<p class="text-[#94a3b8] text-sm">Your personal file storage, powered by AWS</p>
|
||||
</div>
|
||||
|
||||
<!-- Sign in with Google Button -->
|
||||
<a href="{{ url_for('auth.login') }}" class="block w-full">
|
||||
<button class="w-full bg-white hover:bg-gray-100 text-gray-800 font-medium py-3 px-4 rounded-lg flex items-center justify-center space-x-3 transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5">
|
||||
<!-- Google Logo SVG -->
|
||||
<svg class="w-5 h-5" viewBox="0 0 24 24">
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
</svg>
|
||||
<span>Sign in with Google</span>
|
||||
</button>
|
||||
</a>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="mt-8 text-center">
|
||||
<div class="flex items-center justify-center space-x-4 text-sm text-[#94a3b8]">
|
||||
<span>🔒 Secure</span>
|
||||
<span>•</span>
|
||||
<span>🔐 Private</span>
|
||||
<span>•</span>
|
||||
<span>⚡ Fast</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional info -->
|
||||
<div class="mt-6 text-center text-sm text-[#94a3b8]">
|
||||
<p>Powered by AWS S3, RDS, and CloudFront</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user