Code from github to self hosted with git tea in AWS

This commit is contained in:
ADITYANAIR01
2026-03-20 12:01:02 +05:30
commit cf79af8f65
349 changed files with 4761 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
FLASK_SECRET_KEY=your-secret-key-here
FLASK_ENV=production
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GOOGLE_REDIRECT_URI=http://your-ec2-public-ip/auth/callback
AWS_REGION=us-east-1
S3_BUCKET_NAME=your-s3-bucket-name
CLOUDFRONT_DOMAIN=your-cloudfront-domain.cloudfront.net
DB_HOST=your-rds-endpoint.amazonaws.com
DB_PORT=5432
DB_NAME=clouddrive
DB_USER=dbadmin
DB_PASSWORD=your-db-password

375
mini-dropbox/CODE/README.md Normal file
View File

@@ -0,0 +1,375 @@
# AWS Cloud Drive
A modern, full-stack cloud file storage application — a mini Dropbox clone built with Python Flask and AWS services. Users can sign in with Google, upload files to S3, manage them via a beautiful dark-themed UI, and share files with others.
![AWS Cloud Drive](https://img.shields.io/badge/AWS-Cloud%20Drive-blue)
![Python](https://img.shields.io/badge/Python-3.11-green)
![Flask](https://img.shields.io/badge/Flask-3.0-red)
![License](https://img.shields.io/badge/License-MIT-yellow)
---
## 🚀 Features
- **Google OAuth Authentication** — Secure sign-in with Google accounts
- **File Upload & Storage** — Upload files up to 100MB, stored securely on AWS S3
- **File Management** — View, download, delete, and organize your files
- **File Sharing** — Generate shareable links with 7-day expiration
- **File Preview** — Preview images and PDFs directly in browser
- **Modern UI** — Dark theme with smooth animations and responsive design
- **Cloud Infrastructure** — Built on AWS S3, RDS PostgreSQL, and CloudFront
---
## 🏗️ Tech Stack
### Backend
- **Python 3.11** — Core programming language
- **Flask 3.0** — Web framework
- **PostgreSQL (RDS)** — Relational database for metadata
- **AWS S3** — Object storage for files
- **AWS CloudFront** — CDN for fast file delivery
- **Gunicorn** — Production WSGI server
### Frontend
- **HTML5** — Semantic markup
- **Tailwind CSS** — Utility-first CSS framework
- **Vanilla JavaScript** — No frameworks, pure JS
### Authentication
- **Google OAuth 2.0** — Via Authlib
### Python Libraries
- `Flask` — Web framework
- `Authlib` — OAuth client
- `boto3` — AWS SDK for Python
- `psycopg2` — PostgreSQL adapter
- `python-dotenv` — Environment variable management
- `gunicorn` — Production server
---
## 📁 Project Structure
```
aws-cloud-drive/
├── app/
│ ├── __init__.py # Flask app factory
│ ├── config.py # Configuration loader
│ ├── auth.py # Google OAuth blueprint
│ ├── files.py # File management blueprint
│ ├── db.py # Database operations
│ ├── s3.py # S3 operations
│ │
│ ├── templates/
│ │ ├── base.html # Base template with navbar
│ │ ├── login.html # Login page
│ │ └── dashboard.html # Main dashboard
│ │
│ └── static/
│ ├── css/
│ │ └── style.css # Custom styles
│ └── js/
│ └── dashboard.js # Frontend logic
├── sql/
│ └── schema.sql # Database schema
├── .env.example # Environment variables template
├── requirements.txt # Python dependencies
├── run.py # Application entry point
├── gunicorn.conf.py # Gunicorn configuration
└── README.md # This file
```
---
## 🔧 Setup Instructions
### Prerequisites
- Python 3.11+
- PostgreSQL database (AWS RDS recommended)
- AWS account with S3 bucket
- Google Cloud project with OAuth credentials
### 1. Clone the Repository
```bash
git clone https://github.com/yourusername/aws-cloud-drive.git
cd aws-cloud-drive
```
### 2. Install Dependencies
```bash
pip install -r requirements.txt
```
### 3. Configure Environment Variables
Copy `.env.example` to `.env` and fill in your credentials:
```bash
cp .env.example .env
```
Edit `.env`:
```env
FLASK_SECRET_KEY=your-secret-key-here
FLASK_ENV=production
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GOOGLE_REDIRECT_URI=http://your-domain.com/auth/callback
AWS_REGION=us-east-1
S3_BUCKET_NAME=your-s3-bucket-name
CLOUDFRONT_DOMAIN=your-cloudfront-domain.cloudfront.net
DB_HOST=your-rds-endpoint.amazonaws.com
DB_PORT=5432
DB_NAME=clouddrive
DB_USER=dbadmin
DB_PASSWORD=your-db-password
```
### 4. Set Up AWS Resources
#### S3 Bucket
1. Create an S3 bucket in your AWS console
2. Note the bucket name for `.env`
3. Configure IAM role with S3 permissions
#### RDS PostgreSQL
1. Create a PostgreSQL instance in RDS
2. Note the endpoint, port, database name, and credentials
3. Ensure security group allows connections from your EC2 instance
#### CloudFront (Optional)
1. Create a CloudFront distribution pointing to your S3 bucket
2. Note the CloudFront domain for `.env`
#### EC2 IAM Role
Attach an IAM role to your EC2 instance with these policies:
- `AmazonS3FullAccess` (or custom S3 policy)
- `AmazonRDSDataFullAccess` (or custom RDS policy)
### 5. Set Up Google OAuth
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project
3. Enable Google+ API
4. Create OAuth 2.0 credentials:
- Application type: Web application
- Authorized redirect URIs: `http://your-domain.com/auth/callback`
5. Copy Client ID and Client Secret to `.env`
### 6. Initialize Database
The database will be initialized automatically on first run, or manually:
```bash
python -c "from app.db import init_db; init_db()"
```
### 7. Run the Application
#### Development
```bash
python run.py
```
#### Production (with Gunicorn)
```bash
gunicorn -c gunicorn.conf.py run:app
```
---
## 🚀 Deployment on AWS EC2
### 1. Launch EC2 Instance
- **AMI:** Ubuntu 22.04 LTS
- **Instance Type:** t2.micro (free tier) or t2.small
- **Security Group:**
- Allow SSH (port 22)
- Allow HTTP (port 80)
- Allow HTTPS (port 443)
- Allow Custom TCP (port 5000) for testing
### 2. SSH into Instance
```bash
ssh -i your-key.pem ubuntu@your-ec2-public-ip
```
### 3. Install Dependencies
```bash
sudo apt update
sudo apt install python3-pip python3-venv nginx -y
```
### 4. Clone and Set Up Application
```bash
git clone https://github.com/yourusername/aws-cloud-drive.git
cd aws-cloud-drive
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
### 5. Configure Nginx (Optional)
Create `/etc/nginx/sites-available/clouddrive`:
```nginx
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Enable the site:
```bash
sudo ln -s /etc/nginx/sites-available/clouddrive /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx
```
### 6. Run with Gunicorn
Install and configure Gunicorn:
```bash
pip install gunicorn
```
Create a systemd service `/etc/systemd/system/clouddrive.service`:
```ini
[Unit]
Description=Cloud Drive Flask App
After=network.target
[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/aws-cloud-drive
Environment="PATH=/home/ubuntu/aws-cloud-drive/venv/bin"
ExecStart=/home/ubuntu/aws-cloud-drive/venv/bin/gunicorn -c gunicorn.conf.py run:app
[Install]
WantedBy=multi-user.target
```
Start the service:
```bash
sudo systemctl daemon-reload
sudo systemctl start clouddrive
sudo systemctl enable clouddrive
```
---
## 🔒 Security Best Practices
1. **Never commit `.env` file** — Use `.env.example` as a template
2. **Use IAM roles** — Avoid hardcoding AWS credentials
3. **HTTPS only** — Use SSL certificates (Let's Encrypt)
4. **Secure session cookies** — Already configured in app
5. **File size limits** — 100MB enforced
6. **Input validation** — Filenames sanitized with `secure_filename`
7. **User isolation** — All file operations verify ownership
---
## 📝 API Endpoints
### Authentication
- `GET /auth/login` — Redirect to Google OAuth
- `GET /auth/callback` — Handle OAuth callback
- `GET /auth/logout` — Log out user
### Files
- `GET /files/` — Dashboard (requires login)
- `POST /files/upload` — Upload file (requires login)
- `GET /files/download/<id>` — Download file (requires login)
- `POST /files/delete/<id>` — Delete file (requires login)
- `POST /files/share/<id>` — Generate share link (requires login)
- `GET /files/preview/<id>` — Preview file (requires login)
---
## 🎨 UI Screenshots
### Login Page
Clean, modern login with Google OAuth
### Dashboard
Dark-themed file manager with upload, download, share, and delete actions
---
## 🛠️ Development
### Run in Development Mode
```bash
export FLASK_ENV=development
python run.py
```
### Database Schema
The schema is defined in `sql/schema.sql`:
- **users** — User accounts from Google OAuth
- **files** — File metadata with S3 keys and share tokens
---
## 📄 License
This project is licensed under the MIT License.
---
## 🤝 Contributing
Contributions are welcome! Please open an issue or submit a pull request.
---
## 📧 Contact
For questions or support, please open an issue on GitHub.
---
## 🙏 Acknowledgments
- Flask for the excellent web framework
- AWS for reliable cloud infrastructure
- Tailwind CSS for beautiful styling
- Google OAuth for secure authentication
---
**Built with ❤️ using Python, Flask, and AWS**

View 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

View 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'))

View 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
View 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()

View 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
View 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}"

View 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;
}

View 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');
});

View 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>

View 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 %}

View 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>

View File

@@ -0,0 +1,13 @@
# === FILE: gunicorn.conf.py ===
bind = "0.0.0.0:5000"
workers = 2
threads = 2
worker_class = "sync"
worker_connections = 1000
timeout = 120
keepalive = 5
max_requests = 1000
max_requests_jitter = 100
accesslog = "/var/log/gunicorn/access.log"
errorlog = "/var/log/gunicorn/error.log"
loglevel = "info"

View File

@@ -0,0 +1,8 @@
Flask==3.0.0
Authlib==1.3.0
boto3==1.34.0
psycopg[binary]
python-dotenv==1.0.0
gunicorn==21.2.0
Werkzeug==3.0.1
requests==2.31.0

31
mini-dropbox/CODE/run.py Normal file
View File

@@ -0,0 +1,31 @@
# === FILE: run.py ===
import os
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
from app import create_app
from app.db import init_db
# Create Flask app
app = create_app()
if __name__ == '__main__':
# Initialize database on first run
# try:
# init_db()
# except Exception as e:
# print(f"Database initialization warning: {e}")
# print("Make sure database is accessible and schema.sql exists")
print("Skipping database initialization - configure DB connection first")
# Run the application
# host 0.0.0.0 makes the app accessible externally
# port 5000 is the default Flask port
# debug should be False in production
app.run(
host='0.0.0.0',
port=5000,
debug=os.environ.get('FLASK_ENV') == 'development'
)

View File

@@ -0,0 +1,20 @@
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
google_id VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
name VARCHAR(255),
profile_picture TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS files (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
filename VARCHAR(255) NOT NULL,
original_name VARCHAR(255) NOT NULL,
s3_key TEXT NOT NULL,
file_size BIGINT,
file_type VARCHAR(100),
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
share_token VARCHAR(64) UNIQUE
);