Uploaded code

This commit is contained in:
2025-09-27 17:45:52 +01:00
commit b73af5bf11
61 changed files with 10500 additions and 0 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Colby
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

67
MAINTENANCE.md Normal file
View File

@@ -0,0 +1,67 @@
# Maintenance Mode
Sharey includes a maintenance mode feature that allows you to temporarily disable the service and show a maintenance page to users.
## How to Enable Maintenance Mode
```bash
python config_util.py maintenance enable
```
This will:
- Ask for a custom maintenance message (optional)
- Ask for an estimated return time (optional)
- Enable maintenance mode immediately
## How to Disable Maintenance Mode
```bash
python config_util.py maintenance disable
```
## Check Maintenance Status
```bash
python config_util.py maintenance status
```
## Manual Configuration
You can also manually edit `config.json` to enable/disable maintenance mode:
```json
{
"maintenance": {
"enabled": true,
"message": "Sharey is currently under maintenance. Please check back later!",
"estimated_return": "2025-08-22 15:00 UTC"
}
}
```
## What Happens During Maintenance
When maintenance mode is enabled:
-**Health check endpoint** (`/health`) still works (for monitoring)
-**All other routes** show the maintenance page
-**API endpoints** return maintenance page with 503 status
-**File uploads** are disabled
-**File downloads** are disabled
-**Paste creation** is disabled
-**Paste viewing** is disabled
## Maintenance Page Features
- 🎨 **Themed**: Respects user's light/dark theme preference
- 📱 **Responsive**: Works on mobile and desktop
- 🔄 **Refresh button**: Users can easily check if maintenance is over
-**Estimated return time**: Shows when service is expected to return (optional)
- 💬 **Custom message**: Display custom maintenance information
## Use Cases
- **Server updates**: When updating the application
- **Database maintenance**: When performing B2 bucket operations
- **Security issues**: When temporarily disabling service for security reasons
- **Planned downtime**: When performing infrastructure maintenance

237
README.md Normal file
View File

@@ -0,0 +1,237 @@
# Sharey 📁
A secure, modern file sharing platform with configurable storage backends, encryption, themes, and flexible deployment options.
## ✨ Features
- 📁 **File Sharing**: Upload files and get shareable links
- 📝 **Pastebin**: Share text snippets with syntax highlighting
- 💾 **Flexible Storage**: Choose between Backblaze B2 cloud storage or local filesystem
- 🔒 **Client-side Encryption**: Optional encryption for sensitive files
- 🎨 **Dark/Light Themes**: Toggle between themes with preference saving
- 🔗 **Short URLs**: Clean, easy-to-share links
- 🛡️ **Admin Panel**: Secure administration interface
- 🔧 **Maintenance Mode**: Graceful service management
## ⚡ Quick Storage Setup
**Choose your storage type in `config.json`:**
```json
{
"storage": {
"backend": "local" // Use "local" for filesystem or "b2" for cloud
}
}
```
- 🏠 **`"local"`** = Files stored on your server (free, fast)
- ☁️ **`"b2"`** = Files stored in Backblaze B2 cloud (scalable, reliable)
**Test your choice:** `python test_storage.py`
## 🏗️ Project Structure
```
sharey/
├── src/ # Application source code
│ ├── app.py # Main Flask application
│ ├── config.py # Configuration management
│ ├── config_util.py # Configuration utilities
│ ├── static/ # CSS, JS, and static assets
│ └── templates/ # HTML templates
├── tests/ # Test files
├── scripts/ # Utility scripts
│ ├── migrate.py # Database migration
│ ├── setup.py # Setup script
│ ├── setup.sh # Shell setup script
│ └── set_admin_password.py
├── docs/ # Documentation
├── logs/ # Application logs
├── config.json # Application configuration
├── requirements.txt # Python dependencies
├── run.py # Application entry point
└── README.md # This file
```
## 🚀 Quick Start
### Method 1: Enhanced Python Runner (Recommended)
```bash
python run.py
```
This will automatically:
- Check Python version (3.8+ required)
- Create and activate virtual environment if needed
- Install dependencies
- Validate configuration
- Create necessary directories
- Start the application
## 💾 Storage Configuration
Sharey supports multiple storage backends for maximum flexibility. Choose the one that best fits your needs:
### 🔄 **How to Choose Storage Type**
**Method 1: Edit config.json directly (Easiest)**
Open your `config.json` file and change the `backend` field:
```json
{
"storage": {
"backend": "local" // Change to "local" or "b2"
}
}
```
**Method 2: Interactive setup**
```bash
python src/config_util.py set # Interactive configuration
python src/config_util.py validate # Validate your choice
python test_storage.py # Test the storage backend
```
**Method 3: Environment variables**
```bash
export STORAGE_BACKEND=local # or "b2"
```
### Option 1: Backblaze B2 Cloud Storage (Recommended for Production)
```json
{
"storage": {
"backend": "b2"
},
"b2": {
"application_key_id": "your_key_id",
"application_key": "your_application_key",
"bucket_name": "your_bucket_name"
}
}
```
- ✅ Unlimited scalability
- ✅ Built-in redundancy and reliability
- ✅ No local storage requirements
- 💰 Small cost (~$0.005/GB/month)
### Option 2: Local Filesystem Storage (Good for Development/Self-hosting)
```json
{
"storage": {
"backend": "local",
"local_path": "storage"
}
}
```
- ✅ No external dependencies or costs
- ✅ Fast local access
- ✅ Complete data control
- ⚠️ Limited by disk space
- ⚠️ No built-in redundancy
### 🧪 **Test Your Storage Choice**
After changing the storage backend, always test it:
```bash
python src/config_util.py validate # Validate configuration
python test_storage.py # Test storage operations
```
**Quick Test Results:**
-`Storage backend initialized: local` = Local storage working
-`Storage backend initialized: b2` = B2 cloud storage working
-`Storage test failed` = Check your configuration
- ✅ Fast local access
- ✅ Complete data control
- ⚠️ Limited by disk space
- ⚠️ No built-in redundancy
```
**Interactive Configuration:**
```bash
python src/config_util.py set # Interactive setup
python src/config_util.py validate # Validate configuration
python test_storage.py # Test storage backend
```
See [docs/STORAGE.md](docs/STORAGE.md) for complete storage configuration guide.
### Method 2: Python Utility
```bash
# Direct Python usage
python sharey.py start # Full application start
python sharey.py dev # Quick development mode
python sharey.py setup # Set up environment
python sharey.py status # Check system status
python sharey.py clean # Clean temporary files
python sharey.py test # Run tests
python sharey.py logs # Show recent logs
python sharey.py service # Install as system service (Linux)
# Or use the wrapper script (shorter)
./sharey status # Unix/Linux/macOS
```
### Method 3: Manual Setup
```bash
# Create virtual environment
python3 -m venv .venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
# Install dependencies
pip install -r requirements.txt
# Configure storage backend
cp config.json.example config.json
# Edit config.json with your settings
# Run
python src/app.py
```
## 📋 Features
- 🔐 **Security**: Optional file encryption with password protection
- 🎨 **Themes**: Light/dark mode with elegant UI
- ☁️ **Cloud Storage**: Backblaze B2 integration
- 📊 **Progress Tracking**: Real-time upload progress with percentage
- 📱 **Mobile Friendly**: Responsive design for all devices
- 🔗 **Easy Sharing**: Direct links, QR codes, and clipboard integration
- 📝 **Pastebin**: Share text content with syntax highlighting
## 🛠️ Development
### Running Tests
```bash
cd tests
python -m pytest
```
### Development Mode
```bash
# Set debug mode in config.json
python run.py
```
## 📚 Documentation
See the `docs/` directory for:
- Configuration guide
- Deployment instructions
- API documentation
- Migration notes
## 🔧 Configuration
Key configuration options in `config.json`:
- `host`: Server host (default: 0.0.0.0)
- `port`: Server port (default: 5000)
- `debug`: Debug mode (default: false)
- `max_file_size`: Maximum upload size
- `b2_*`: Backblaze B2 credentials
## 📄 License
See LICENSE file for details.

73
STORAGE_QUICKSTART.md Normal file
View File

@@ -0,0 +1,73 @@
# 🚀 Storage Quick Start
**Just want to get started quickly? Here's how to choose your storage type:**
## 📁 Local Storage (Easiest)
**Best for:** Development, testing, small self-hosted setups
1. **Edit config.json:**
```json
{
"storage": {
"backend": "local"
}
}
```
2. **Test it:**
```bash
python test_storage.py
```
3. **Done!** Files will be stored in the `storage/` folder.
## ☁️ Cloud Storage (B2)
**Best for:** Production, scaling, reliability
1. **Get B2 credentials:**
- Sign up at [backblaze.com](https://www.backblaze.com/b2/)
- Create a bucket
- Create application key
2. **Edit config.json:**
```json
{
"storage": {
"backend": "b2"
},
"b2": {
"application_key_id": "your_key_id",
"application_key": "your_application_key",
"bucket_name": "your_bucket_name"
}
}
```
3. **Test it:**
```bash
python test_storage.py
```
4. **Done!** Files will be stored in your B2 bucket.
## 🔄 Switch Anytime
Want to switch? Just change the `"backend"` field:
```json
{
"storage": {
"backend": "local" // or "b2"
}
}
```
Then run `python test_storage.py` to verify.
## 🆘 Need Help?
- **Validation errors?** Run `python src/config_util.py validate`
- **Want interactive setup?** Run `python src/config_util.py set`
- **Full documentation:** See [docs/STORAGE.md](docs/STORAGE.md)

Binary file not shown.

Binary file not shown.

Binary file not shown.

131
cleanup_daemon.py Executable file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""
Cleanup daemon for expired files and pastes in Sharey
Runs continuously and checks for expired files at regular intervals
Supports both local storage and Backblaze B2 storage backends
"""
import os
import sys
import json
import sqlite3
import time
import signal
from datetime import datetime, timezone
from pathlib import Path
# Add the project root to Python path
project_root = Path(__file__).parent
sys.path.append(str(project_root))
from src.config import config
from src.storage import StorageManager
from expiry_db import ExpiryDatabase
# Configuration
CLEANUP_INTERVAL = 60 # seconds (1 minute)
CHECK_INTERVAL = 5 # seconds between startup checks
# Global flag for graceful shutdown
running = True
def signal_handler(signum, frame):
"""Handle shutdown signals gracefully"""
global running
print(f"\n🛑 Received signal {signum}, shutting down gracefully...")
running = False
def cleanup_expired_files():
"""Perform cleanup of expired files (same logic as cleanup script)"""
try:
# Initialize storage manager
storage_manager = StorageManager(config)
backend_info = storage_manager.get_backend_info()
# Initialize expiry database
expiry_db = ExpiryDatabase()
# Get expired files from database
expired_files = expiry_db.get_expired_files()
if not expired_files:
print(f"{datetime.now().strftime('%H:%M:%S')} - No expired files found")
return 0
print(f"🗑️ {datetime.now().strftime('%H:%M:%S')} - Found {len(expired_files)} expired files to delete")
deleted_count = 0
for file_info in expired_files:
file_path = file_info['file_path']
expires_at = file_info['expires_at']
try:
print(f"🗑️ Deleting: {file_path} (expired: {expires_at})")
# Delete from storage backend
if storage_manager.delete_file(file_path):
# Remove from expiry database
expiry_db.remove_file(file_path)
deleted_count += 1
print(f"✅ Deleted: {file_path}")
else:
print(f"❌ Failed to delete: {file_path}")
except Exception as e:
print(f"❌ Error deleting {file_path}: {e}")
if deleted_count > 0:
print(f"✅ Cleanup completed! Deleted {deleted_count} expired files")
return deleted_count
except Exception as e:
print(f"❌ Cleanup error: {e}")
return -1
def main():
"""Main daemon function"""
# Set up signal handlers for graceful shutdown
signal.signal(signal.SIGINT, signal_handler) # Ctrl+C
signal.signal(signal.SIGTERM, signal_handler) # systemctl stop
print("🧹 Sharey Cleanup Daemon")
print("=" * 40)
print(f"🚀 Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"⏰ Cleanup interval: {CLEANUP_INTERVAL} seconds")
print(f"📁 Storage backend: {config.get('storage', {}).get('backend', 'b2')}")
print(f"🗄️ Database: expiry.db")
print("=" * 40)
print("Press Ctrl+C to stop")
print()
last_cleanup = 0
try:
while running:
current_time = time.time()
# Check if it's time for cleanup
if current_time - last_cleanup >= CLEANUP_INTERVAL:
try:
cleanup_expired_files()
last_cleanup = current_time
except Exception as e:
print(f"❌ Cleanup cycle failed: {e}")
# Sleep for a short interval to avoid busy waiting
time.sleep(CHECK_INTERVAL)
except KeyboardInterrupt:
pass # Handle Ctrl+C gracefully
print(f"\n🛑 Cleanup daemon stopped at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("👋 Goodbye!")
if __name__ == "__main__":
try:
main()
except Exception as e:
print(f"❌ Daemon failed to start: {e}")
sys.exit(1)
sys.exit(0)

154
cleanup_expired.py Executable file
View File

@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""
Cleanup script for expired files and pastes in Sharey
Supports both local storage and Backblaze B2 storage backends
"""
import os
import sys
import json
from datetime import datetime, timezone
from pathlib import Path
# Add src directory to Python path
src_path = Path(__file__).parent / "src"
sys.path.insert(0, str(src_path))
from config import config
from storage import StorageManager
def cleanup_local_storage(storage_manager):
"""Clean up expired files from local storage"""
print("🧹 Cleaning up local storage...")
backend = storage_manager.backend
base_path = backend.base_path
deleted_count = 0
# Check files directory
files_dir = base_path / "files"
if files_dir.exists():
for file_path in files_dir.glob("*"):
if file_path.is_file() and not file_path.name.endswith('.meta'):
meta_file = file_path.with_suffix(file_path.suffix + '.meta')
if meta_file.exists():
try:
with open(meta_file, 'r') as f:
metadata = json.load(f)
expires_at = metadata.get('expires_at')
if expires_at:
expiry_time = datetime.fromisoformat(expires_at.replace('Z', '+00:00'))
if datetime.now() > expiry_time:
# Delete both file and metadata
file_path.unlink()
meta_file.unlink()
print(f"🗑️ Deleted expired file: {file_path.name}")
deleted_count += 1
except Exception as e:
print(f"❌ Error processing {file_path.name}: {e}")
# Check pastes directory
pastes_dir = base_path / "pastes"
if pastes_dir.exists():
for file_path in pastes_dir.glob("*.txt"):
if file_path.is_file():
meta_file = file_path.with_suffix('.txt.meta')
if meta_file.exists():
try:
with open(meta_file, 'r') as f:
metadata = json.load(f)
expires_at = metadata.get('expires_at')
if expires_at:
expiry_time = datetime.fromisoformat(expires_at.replace('Z', '+00:00'))
if datetime.now() > expiry_time:
# Delete both file and metadata
file_path.unlink()
meta_file.unlink()
print(f"🗑️ Deleted expired paste: {file_path.name}")
deleted_count += 1
except Exception as e:
print(f"❌ Error processing {file_path.name}: {e}")
return deleted_count
def cleanup_b2_storage(storage_manager):
"""Clean up expired files from Backblaze B2 storage"""
print("🧹 Cleaning up B2 storage...")
deleted_count = 0
try:
# List all files using the storage manager
all_files = storage_manager.list_files("")
for file_path in all_files:
try:
# Get metadata for this file
metadata = storage_manager.get_metadata(file_path)
if not metadata:
continue
expires_at = metadata.get('expires_at')
if expires_at:
expiry_time = datetime.fromisoformat(expires_at.replace('Z', '+00:00'))
current_time = datetime.now(timezone.utc).replace(tzinfo=None)
if current_time > expiry_time:
print(f"🗑️ Deleting expired B2 file: {file_path}")
if storage_manager.delete_file(file_path):
deleted_count += 1
else:
print(f"❌ Failed to delete B2 file: {file_path}")
except Exception as e:
print(f"❌ Error processing B2 file {file_path}: {e}")
except Exception as e:
print(f"❌ Error listing B2 files: {e}")
return 0
return deleted_count
def main():
"""Main cleanup function"""
print("🧹 Sharey Cleanup Script")
print("=" * 40)
print(f"⏰ Started at: {datetime.now().isoformat()}")
try:
# Initialize storage manager
storage_manager = StorageManager(config)
backend_info = storage_manager.get_backend_info()
print(f"📁 Storage backend: {backend_info['type']}")
# Run appropriate cleanup based on storage type
if backend_info['type'] == 'local':
deleted_count = cleanup_local_storage(storage_manager)
elif backend_info['type'] == 'b2':
deleted_count = cleanup_b2_storage(storage_manager)
else:
print(f"❌ Unknown storage backend: {backend_info['type']}")
sys.exit(1)
print("=" * 40)
if deleted_count > 0:
print(f"✅ Cleanup completed! Deleted {deleted_count} expired items.")
else:
print("✅ Cleanup completed! No expired items found.")
print(f"⏰ Finished at: {datetime.now().isoformat()}")
except Exception as e:
print(f"❌ Cleanup failed: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

88
cleanup_expired_db.py Executable file
View File

@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""
Cleanup script for expired files using expiry database
Works with both local storage and Backblaze B2 storage backends
"""
import os
import sys
from datetime import datetime
from pathlib import Path
# Add the project root to Python path
project_root = Path(__file__).parent
sys.path.append(str(project_root))
from src.config import config
from src.storage import StorageManager
from expiry_db import ExpiryDatabase
def main():
"""Main cleanup function"""
print("🧹 Sharey Cleanup Script (Database-driven)")
print("=" * 50)
print(f"⏰ Started at: {datetime.utcnow().isoformat()}")
try:
# Load configuration
print("✅ Loaded configuration from config.json")
# Initialize storage manager
storage_manager = StorageManager(config)
backend_info = storage_manager.get_backend_info()
print(f"📁 Storage backend: {backend_info['type']}")
# Initialize expiry database
expiry_db = ExpiryDatabase()
# Get expired files from database
expired_files = expiry_db.get_expired_files()
if not expired_files:
print("✅ No expired files found.")
return 0
print(f"🗑️ Found {len(expired_files)} expired files to delete:")
deleted_count = 0
for file_info in expired_files:
file_path = file_info['file_path']
expires_at = file_info['expires_at']
storage_backend = file_info['storage_backend']
try:
print(f"🗑️ Deleting expired file: {file_path} (expired: {expires_at})")
# Delete from storage backend
if storage_manager.delete_file(file_path):
# Remove from expiry database
expiry_db.remove_file(file_path)
deleted_count += 1
print(f"✅ Deleted: {file_path}")
else:
print(f"❌ Failed to delete from storage: {file_path}")
except Exception as e:
print(f"❌ Error deleting {file_path}: {e}")
print("=" * 50)
print(f"✅ Cleanup completed! Deleted {deleted_count} expired files.")
# Show database stats
stats = expiry_db.get_stats()
print(f"📊 Database stats:")
print(f" • Total tracked files: {stats.get('total_files', 0)}")
print(f" • Active files: {stats.get('active_files', 0)}")
print(f" • Expired files: {stats.get('expired_files', 0)}")
return deleted_count
except Exception as e:
print(f"❌ Cleanup failed: {e}")
return -1
finally:
print(f"⏰ Finished at: {datetime.utcnow().isoformat()}")
if __name__ == "__main__":
result = main()
sys.exit(0 if result >= 0 else 1)

36
config.json Normal file
View File

@@ -0,0 +1,36 @@
{
"storage": {
"backend": "b2",
"local_path": "storage"
},
"b2": {
"application_key_id": "0023cd3169723c50000000001",
"application_key": "K002rXzeiUfyfpEabzU3ikvnJy5FU1M",
"bucket_name": "sharey"
},
"flask": {
"host": "0.0.0.0",
"port": 8866,
"debug": true
},
"upload": {
"max_file_size_mb": 100
},
"paste": {
"max_length": 1000000
},
"security": {
"rate_limit_enabled": false,
"max_uploads_per_hour": 50
},
"maintenance": {
"enabled": false,
"message": "Sharey is currently under maintenance. Please check back later!",
"estimated_return": ""
},
"admin": {
"enabled": true,
"password_hash": "240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9",
"session_timeout_minutes": 60
}
}

44
config.json.example Normal file
View File

@@ -0,0 +1,44 @@
{
"storage": {
"backend": "b2",
"local_path": "storage"
},
"b2": {
"application_key_id": "your_key_id_here",
"application_key": "your_application_key_here",
"bucket_name": "your_bucket_name_here"
},
"flask": {
"host": "127.0.0.1",
"port": 8866,
"debug": true
},
"upload": {
"max_file_size_mb": 100,
"allowed_extensions": [".jpg", ".jpeg", ".png", ".gif", ".pdf", ".txt", ".doc", ".docx", ".zip", ".mp4", ".mp3"],
"default_expiry": "7d"
},
"paste": {
"max_length": 1000000,
"default_expiry": "24h"
},
"expiry": {
"enabled": true,
"cleanup_interval_hours": 1,
"available_options": ["1h", "24h", "7d", "30d", "90d", "never"]
},
"security": {
"rate_limit_enabled": false,
"max_uploads_per_hour": 50
},
"maintenance": {
"enabled": false,
"message": "Sharey is currently under maintenance. Please check back later!",
"estimated_return": ""
},
"admin": {
"enabled": true,
"password_hash": "",
"session_timeout_minutes": 60
}
}

15
cron_example.txt Normal file
View File

@@ -0,0 +1,15 @@
# Cron job example for automatic file cleanup
# This will run the cleanup script every hour to remove expired files
# Edit your crontab with: crontab -e
# Add this line to run cleanup every hour:
0 * * * * cd /home/colby/sharey && python3 cleanup_expired.py >> logs/cleanup.log 2>&1
# Alternative schedules:
# Every 15 minutes: */15 * * * * cd /home/colby/sharey && python3 cleanup_expired.py >> logs/cleanup.log 2>&1
# Every 6 hours: 0 */6 * * * cd /home/colby/sharey && python3 cleanup_expired.py >> logs/cleanup.log 2>&1
# Once daily at 2 AM: 0 2 * * * cd /home/colby/sharey && python3 cleanup_expired.py >> logs/cleanup.log 2>&1
# For production, adjust the path to your actual installation directory
# Example for production:
# 0 * * * * cd /var/www/sharey && /usr/bin/python3 cleanup_expired.py >> logs/cleanup.log 2>&1

147
docs/CONFIG_SYSTEM.md Normal file
View File

@@ -0,0 +1,147 @@
# Sharey Configuration System
## Overview
Sharey now features a comprehensive JSON-based configuration system that replaces environment variables with a more structured and feature-rich approach.
## Configuration Files
### Primary Configuration: `config.json`
```json
{
"b2": {
"application_key_id": "your_key_id_here",
"application_key": "your_application_key_here",
"bucket_name": "your_bucket_name_here"
},
"flask": {
"host": "127.0.0.1",
"port": 8866,
"debug": true
},
"upload": {
"max_file_size_mb": 100,
"allowed_extensions": [".jpg", ".jpeg", ".png", ".gif", ".pdf", ".txt", ".doc", ".docx", ".zip", ".mp4", ".mp3"]
},
"paste": {
"max_length": 1000000
},
"security": {
"rate_limit_enabled": false,
"max_uploads_per_hour": 50
}
}
```
### Fallback: Environment Variables
For backwards compatibility, Sharey still supports `.env` files and environment variables.
## Configuration Management Tools
### Setup Script: `setup.py`
- Creates `config.json` from template
- Sets up virtual environment
- Installs dependencies
- Validates configuration
### Configuration Utility: `config_util.py`
```bash
python config_util.py show # Display current config
python config_util.py validate # Validate configuration
python config_util.py set # Interactive setup
python config_util.py reset # Reset to defaults
```
### Test Utility: `test_b2.py`
- Tests B2 connection
- Validates credentials
- Shows configuration summary
## New Features
### File Upload Controls
- **File size limits**: Configurable max file size in MB
- **File type restrictions**: Whitelist of allowed extensions
- **Validation**: Automatic file type checking
### Paste Controls
- **Length limits**: Configurable maximum paste length
- **Validation**: Automatic length checking
### Flask Configuration
- **Host/Port**: Configurable server settings
- **Debug mode**: Environment-specific settings
### Security Features (Future)
- **Rate limiting**: Configurable upload limits
- **Extensible**: Ready for additional security features
## Configuration Loading Priority
1. **config.json** (if exists)
2. **Environment variables** (fallback)
3. **Built-in defaults** (last resort)
## Benefits of JSON Configuration
### ✅ **Structured Data**
- Hierarchical organization
- Type safety (strings, numbers, booleans, arrays)
- Better validation
### ✅ **Feature Rich**
- Upload restrictions
- File type filtering
- Paste length limits
- Server configuration
### ✅ **User Friendly**
- Interactive setup utility
- Configuration validation
- Clear error messages
- Comprehensive documentation
### ✅ **Developer Friendly**
- Easy to extend
- Version controllable (with secrets excluded)
- IDE support with syntax highlighting
- Better tooling support
### ✅ **Backwards Compatible**
- Still supports .env files
- Smooth migration path
- No breaking changes
## Migration from .env
Old `.env` approach:
```env
B2_APPLICATION_KEY_ID=key123
B2_APPLICATION_KEY=secret456
B2_BUCKET_NAME=mybucket
```
New `config.json` approach:
```json
{
"b2": {
"application_key_id": "key123",
"application_key": "secret456",
"bucket_name": "mybucket"
}
}
```
## Quick Start
1. **Run setup**: `python setup.py`
2. **Configure**: `python config_util.py set`
3. **Test**: `python test_b2.py`
4. **Run**: `python app.py`
## Security Considerations
- `config.json` is excluded from git by default
- Sensitive data can be hidden in display utilities
- Environment variable fallback for production deployments
- Configuration validation prevents common mistakes

79
docs/DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,79 @@
# Deployment Configuration
## Environment Variables for Production
When deploying Sharey to production, make sure to set these environment variables:
```bash
# Required B2 Configuration
export B2_APPLICATION_KEY_ID="your_key_id"
export B2_APPLICATION_KEY="your_key"
export B2_BUCKET_NAME="your_bucket_name"
# Flask Configuration
export FLASK_ENV="production"
export FLASK_DEBUG="False"
```
## Docker Deployment
### Dockerfile
```dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8866
CMD ["python", "app.py"]
```
### Docker Compose
```yaml
version: '3.8'
services:
sharey:
build: .
ports:
- "8866:8866"
environment:
- B2_APPLICATION_KEY_ID=${B2_APPLICATION_KEY_ID}
- B2_APPLICATION_KEY=${B2_APPLICATION_KEY}
- B2_BUCKET_NAME=${B2_BUCKET_NAME}
volumes:
- .env:/app/.env
```
## Nginx Reverse Proxy
```nginx
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://127.0.0.1:8866;
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;
# Increase client max body size for file uploads
client_max_body_size 100M;
}
}
```
## Production Considerations
1. **File Size Limits**: Configure your web server to handle large file uploads
2. **HTTPS**: Use SSL/TLS certificates for secure file transfers
3. **Rate Limiting**: Implement rate limiting to prevent abuse
4. **Monitoring**: Set up logging and monitoring for the application
5. **Backup**: B2 provides versioning, but consider backup strategies
6. **Security**: Restrict B2 bucket access and use environment variables for secrets

67
docs/MAINTENANCE.md Normal file
View File

@@ -0,0 +1,67 @@
# Maintenance Mode
Sharey includes a maintenance mode feature that allows you to temporarily disable the service and show a maintenance page to users.
## How to Enable Maintenance Mode
```bash
python config_util.py maintenance enable
```
This will:
- Ask for a custom maintenance message (optional)
- Ask for an estimated return time (optional)
- Enable maintenance mode immediately
## How to Disable Maintenance Mode
```bash
python config_util.py maintenance disable
```
## Check Maintenance Status
```bash
python config_util.py maintenance status
```
## Manual Configuration
You can also manually edit `config.json` to enable/disable maintenance mode:
```json
{
"maintenance": {
"enabled": true,
"message": "Sharey is currently under maintenance. Please check back later!",
"estimated_return": "2025-08-22 15:00 UTC"
}
}
```
## What Happens During Maintenance
When maintenance mode is enabled:
-**Health check endpoint** (`/health`) still works (for monitoring)
-**All other routes** show the maintenance page
-**API endpoints** return maintenance page with 503 status
-**File uploads** are disabled
-**File downloads** are disabled
-**Paste creation** is disabled
-**Paste viewing** is disabled
## Maintenance Page Features
- 🎨 **Themed**: Respects user's light/dark theme preference
- 📱 **Responsive**: Works on mobile and desktop
- 🔄 **Refresh button**: Users can easily check if maintenance is over
-**Estimated return time**: Shows when service is expected to return (optional)
- 💬 **Custom message**: Display custom maintenance information
## Use Cases
- **Server updates**: When updating the application
- **Database maintenance**: When performing B2 bucket operations
- **Security issues**: When temporarily disabling service for security reasons
- **Planned downtime**: When performing infrastructure maintenance

118
docs/MIGRATION_SUMMARY.md Normal file
View File

@@ -0,0 +1,118 @@
# Sharey B2 Migration Summary
## What Changed
Your Sharey application has been successfully modified to use Backblaze B2 cloud storage instead of local file storage. Here are the key changes:
### 🔄 Core Changes
1. **Storage Backend**:
- Removed local file storage (`uploads/` and `pastes/` folders)
- Added Backblaze B2 cloud storage integration
- Files and pastes now stored in B2 bucket
2. **Dependencies Added**:
- `b2sdk`: Official Backblaze B2 SDK
- `python-dotenv`: Environment variable management
3. **Configuration**:
- Added `.env` file support for B2 credentials
- Added connection validation and error handling
- Added health check endpoint
### 📁 New Files Created
- `requirements.txt` - Python dependencies
- `.env.example` - Environment template
- `setup.py` - Automated setup script (Python-based)
- `test_b2.py` - B2 connection testing utility
- `DEPLOYMENT.md` - Production deployment guide
- Updated `.gitignore` - Improved git ignore rules
- Updated `README.md` - Comprehensive setup instructions
### 🔧 Modified Functions
1. **File Upload** (`/api/upload`):
- Now uploads files directly to B2 bucket under `files/` prefix
- Returns B2 direct download URLs
- Better error handling
2. **File Serving** (`/files/<file_id>`):
- Now redirects to B2 download URLs
- No longer serves files locally
3. **Paste Creation** (`/api/paste`):
- Stores paste content in B2 under `pastes/` prefix
- UTF-8 encoding support
4. **Paste Viewing** (`/pastes/<paste_id>` and `/pastes/raw/<paste_id>`):
- Downloads paste content from B2
- Maintains same user interface
### 🏗️ Bucket Organization
Your B2 bucket will be organized as:
```
your-bucket/
├── files/ # Uploaded files (images, documents, etc.)
│ ├── abc123.jpg
│ ├── def456.pdf
│ └── ...
└── pastes/ # Text pastes
├── ghi789.txt
├── jkl012.txt
└── ...
```
## Next Steps
1. **Get B2 Credentials**:
- Sign up at [Backblaze B2](https://www.backblaze.com/b2)
- Create application key and bucket
- Note down: Key ID, Application Key, Bucket Name
2. **Configure Environment**:
```bash
cp .env.example .env
# Edit .env with your B2 credentials
```
3. **Install Dependencies**:
```bash
python setup.py # or manually: pip install -r requirements.txt
```
4. **Test Configuration**:
```bash
python test_b2.py
```
5. **Run Application**:
```bash
python app.py
```
## Benefits of B2 Storage
- ✅ **Scalable**: No local disk space limitations
- ✅ **Reliable**: Built-in redundancy and backups
- ✅ **Cost-effective**: Pay only for what you use
- ✅ **Global CDN**: Fast downloads worldwide
- ✅ **Secure**: Encrypted storage with access controls
- ✅ **Maintenance-free**: No local file management needed
## Migration Notes
- Existing local files/pastes will remain in local folders
- New uploads will go to B2
- Old URLs will break (files are now served from B2)
- No data migration script provided (manual migration needed if desired)
- B2 bucket should be set to "Public" for file sharing to work properly
## Support
If you encounter issues:
1. Run `python test_b2.py` to test your configuration
2. Check the health endpoint: `http://localhost:8866/health`
3. Verify B2 bucket permissions and settings
4. Ensure your .env file has correct credentials

99
docs/ORGANIZATION.md Normal file
View File

@@ -0,0 +1,99 @@
# Project Organization Summary
## 📁 Reorganized Structure
The Sharey project has been reorganized into a clean, professional structure:
### 🏗️ Directory Structure
```
sharey/
├── src/ # Main application code
│ ├── app.py # Flask application
│ ├── config.py # Configuration management
│ ├── config_util.py # Config utilities
│ ├── static/ # CSS, JS, assets
│ │ ├── script.js # Main JavaScript
│ │ ├── style.css # Stylesheets
│ │ └── script_backup.js
│ └── templates/ # HTML templates
│ ├── index.html # Main page
│ ├── admin.html # Admin panel
│ ├── admin_login.html
│ ├── maintenance.html
│ ├── view_file.html # File viewer
│ └── view_paste.html # Paste viewer
├── tests/ # Test files
│ ├── test_b2.py
│ ├── test_bucket_contents.py
│ └── test_paste.py
├── scripts/ # Utility scripts
│ ├── clean.sh # Cleanup script
│ ├── migrate.py # Database migration
│ ├── set_admin_password.py
│ ├── setup.py
│ └── setup.sh
├── docs/ # Documentation
│ ├── CONFIG_SYSTEM.md
│ ├── DEPLOYMENT.md
│ ├── MAINTENANCE.md
│ ├── MIGRATION_SUMMARY.md
│ └── README.md (old)
├── logs/ # Application logs
│ ├── app.log
│ └── migration_log_20250815_121855.txt
├── config.json # Main configuration
├── config.json.example # Configuration template
├── requirements.txt # Python dependencies
├── run.py # Application entry point
├── dev-setup.sh # Development setup
└── README.md # Project documentation
```
## 🚀 Running the Application
### New Entry Point
```bash
python run.py
```
### Development Setup
```bash
./dev-setup.sh
```
### Cleanup
```bash
./scripts/clean.sh
```
## ✅ Changes Made
1. **Moved core application** (`app.py`, `config.py`) to `src/`
2. **Organized static assets** (`static/`, `templates/`) under `src/`
3. **Collected tests** in dedicated `tests/` directory
4. **Grouped scripts** in `scripts/` directory
5. **Centralized documentation** in `docs/` directory
6. **Created logs directory** for log files
7. **Added entry point** (`run.py`) for easy execution
8. **Created development setup** script for easy onboarding
9. **Added cleanup script** for maintenance
10. **Removed temporary files** and debug artifacts
11. **Updated README.md** with new structure
## 🎯 Benefits
- **Cleaner root directory** - Only essential files at project root
- **Logical grouping** - Related files organized together
- **Professional structure** - Follows Python project best practices
- **Easy navigation** - Clear separation of concerns
- **Better maintainability** - Easier to find and modify files
- **Development friendly** - Scripts for common tasks
## 🔧 Next Steps
1. Update any deployment scripts to use new structure
2. Test the new entry point thoroughly
3. Update CI/CD pipelines if applicable
4. Consider adding more development tools (linting, formatting)
The project is now much cleaner and follows modern Python project conventions!

196
docs/README.md Normal file
View File

@@ -0,0 +1,196 @@
# Sharey
A simple file sharing and pastebin service using Backblaze B2 cloud storage.
## Features
- 📁 **File Sharing**: Upload files and get shareable links
- 📝 **Pastebin**: Share text snippets with syntax highlighting support
- ☁️ **Cloud Storage**: Files stored securely in Backblaze B2
- 🔗 **Short URLs**: Clean, easy-to-share links
- 🎨 **Clean UI**: Simple, responsive web interface
## Setup
### Prerequisites
- Python 3.7+
- Backblaze B2 account with:
- Application Key ID
- Application Key
- Bucket name
### Installation
1. **Clone the repository**
```bash
git clone <your-repo-url>
cd sharey
```
2. **Run the setup script**
```bash
python setup.py
```
This will automatically create a virtual environment and install dependencies.
3. **Configure B2 credentials**
Sharey supports two configuration methods:
**Option 1: JSON Configuration (Recommended)**
```bash
# Edit config.json with your credentials
nano config.json
```
**Option 2: Environment Variables**
```bash
# Edit .env file with your credentials
nano .env
```
**Interactive Configuration**
```bash
python config_util.py set
```
4. **Test B2 connection**
```bash
python test_b2.py
```
5. **Run the application**
```bash
# If using virtual environment (recommended)
source venv/bin/activate
python app.py
# Or directly
venv/bin/python app.py
```
The application will be available at `http://127.0.0.1:8866`
## Backblaze B2 Setup
1. **Create a Backblaze B2 account** at [backblaze.com](https://www.backblaze.com/b2/cloud-storage.html)
2. **Create an application key**:
- Go to [App Keys](https://secure.backblaze.com/app_keys.htm)
- Click "Add a New Application Key"
- Name it "Sharey" or similar
- Choose appropriate permissions (read/write access to your bucket)
- Save the Key ID and Application Key
3. **Create a bucket**:
- Go to [Buckets](https://secure.backblaze.com/b2_buckets.htm)
- Click "Create a Bucket"
- Choose "Public" if you want files to be publicly accessible
- Note the bucket name
4. **Configure bucket for public access** (if desired):
- Set bucket type to "Public"
- Configure CORS settings if needed for web access
## File Organization in B2
Files are organized in your B2 bucket as follows:
```
your-bucket/
├── files/
│ ├── abc123.jpg
│ ├── def456.pdf
│ └── ...
└── pastes/
├── ghi789.txt
├── jkl012.txt
└── ...
```
## Development
### Manual Installation
If you prefer not to use the setup script:
```bash
# Install dependencies
pip install -r requirements.txt
# Copy environment template
cp .env.example .env
# Edit .env with your credentials
nano .env
# Test configuration
python test_b2.py
# Run the app
python app.py
```
## Configuration
Sharey uses a flexible configuration system that supports both JSON files and environment variables.
### Configuration Files
- `config.json` - Main configuration file (recommended)
- `.env` - Environment variables (fallback/legacy support)
### Configuration Management
**View current configuration:**
```bash
python config_util.py show
```
**Interactive setup:**
```bash
python config_util.py set
```
**Validate configuration:**
```bash
python config_util.py validate
```
**Reset to defaults:**
```bash
python config_util.py reset
```
### Configuration Options
```json
{
"b2": {
"application_key_id": "your_key_id_here",
"application_key": "your_application_key_here",
"bucket_name": "your_bucket_name_here"
},
"flask": {
"host": "127.0.0.1",
"port": 8866,
"debug": true
},
"upload": {
"max_file_size_mb": 100,
"allowed_extensions": [".jpg", ".jpeg", ".png", ".gif", ".pdf", ".txt", ".doc", ".docx", ".zip", ".mp4", ".mp3"]
},
"paste": {
"max_length": 1000000
},
"security": {
"rate_limit_enabled": false,
"max_uploads_per_hour": 50
}
}
```
## License
MIT License - see [LICENSE](LICENSE) file for details.

375
docs/STORAGE.md Normal file
View File

@@ -0,0 +1,375 @@
# Storage Configuration Guide
Sharey supports multiple storage backends for maximum flexibility. You can choose between cloud storage (Backblaze B2) or local filesystem storage.
## 🔄 How to Choose Storage Type
### **Quick Method: Edit config.json**
1. Open your `config.json` file
2. Find the `"storage"` section
3. Change `"backend"` to either `"local"` or `"b2"`
```json
{
"storage": {
"backend": "local" // Change this to "local" or "b2"
}
}
```
### **Interactive Method**
```bash
python src/config_util.py set # Interactive setup
python src/config_util.py validate # Verify configuration
python test_storage.py # Test the backend
```
### **Environment Variable Method**
```bash
export STORAGE_BACKEND=local # or "b2"
```
## 📊 Storage Backend Comparison
| Feature | **Local Storage** | **B2 Cloud Storage** |
|---------|------------------|-------------------|
| **Setup Difficulty** | ✅ Easy | ⚠️ Requires B2 account |
| **Cost** | ✅ Free | 💰 ~$0.005/GB/month |
| **Speed** | ✅ Very fast | ⚠️ Network dependent |
| **Reliability** | ⚠️ Single machine | ✅ Cloud redundancy |
| **Scalability** | ⚠️ Disk space limited | ✅ Unlimited |
| **Best for** | Development, self-hosting | Production, scaling |
## Storage Backends
### 1. Backblaze B2 (Cloud Storage)
-**Best for production deployments**
-**Scalable and reliable**
-**No local disk space requirements**
-**Requires B2 account and credentials**
### 2. Local Filesystem
-**No external dependencies**
-**Fast access**
-**No cloud costs**
-**Limited by local disk space**
-**Not suitable for distributed deployments**
## Configuration Details
## Configuration Details
### Option 1: config.json (Recommended)
```json
{
"storage": {
"backend": "b2", // "b2" or "local"
"local_path": "storage" // Path for local storage (only used if backend is "local")
},
"b2": {
"application_key_id": "your_key_id_here",
"application_key": "your_application_key_here",
"bucket_name": "your_bucket_name_here"
}
}
```
### Option 2: Environment Variables
```bash
# Storage configuration
export STORAGE_BACKEND=local
export STORAGE_LOCAL_PATH=./my_storage
# B2 configuration (only needed if using B2)
export B2_APPLICATION_KEY_ID=your_key_id_here
export B2_APPLICATION_KEY=your_application_key_here
export B2_BUCKET_NAME=your_bucket_name_here
```
## 🚀 Quick Start Guide
### **For Development/Testing (Local Storage)**
```bash
# 1. Edit config.json
{
"storage": {
"backend": "local"
}
}
# 2. Test it
python test_storage.py
# 3. Start the app
python run.py
```
Files will be stored in the `storage/` directory.
### **For Production (B2 Cloud Storage)**
```bash
# 1. Get B2 credentials from backblaze.com
# 2. Edit config.json
{
"storage": {
"backend": "b2"
},
"b2": {
"application_key_id": "your_key_id",
"application_key": "your_key",
"bucket_name": "your_bucket"
}
}
# 3. Test it
python test_storage.py
# 4. Start the app
python run.py
```
## Setup Examples
### Using Local Storage
1. **Edit config.json:**
```json
{
"storage": {
"backend": "local",
"local_path": "storage"
}
}
```
2. **Start the application:**
```bash
python run.py
```
The application will automatically create the `storage` directory and subdirectories as needed.
### Using Backblaze B2
1. **Create a B2 account** at [backblaze.com](https://www.backblaze.com/b2/)
2. **Create a bucket** and application key with read/write permissions
3. **Edit config.json:**
```json
{
"storage": {
"backend": "b2"
},
"b2": {
"application_key_id": "your_actual_key_id",
"application_key": "your_actual_key",
"bucket_name": "your_bucket_name"
}
}
```
4. **Start the application:**
```bash
python run.py
```
## Interactive Configuration
Use the configuration utility for easy setup:
```bash
# Configure storage interactively
python src/config_util.py set
# Validate your configuration
python src/config_util.py validate
# Show current configuration
python src/config_util.py show
```
## Testing Storage
Test your storage configuration:
```bash
# Test storage backend
python test_storage.py
```
This will:
- ✅ Initialize the storage backend
- ✅ Upload a test file
- ✅ Download and verify the file
- ✅ Test file operations (exists, size, list)
- ✅ Clean up test files
## Migration Between Backends
### From B2 to Local Storage
1. **Download your data** from B2 (optional - keep as backup)
2. **Update config.json:**
```json
{
"storage": {
"backend": "local",
"local_path": "storage"
}
}
```
3. **Restart the application**
**Note:** Existing files in B2 won't be automatically transferred. URLs will break unless you migrate the data.
### From Local to B2 Storage
1. **Set up B2 bucket and credentials**
2. **Update config.json:**
```json
{
"storage": {
"backend": "b2"
},
"b2": {
"application_key_id": "your_key_id",
"application_key": "your_key",
"bucket_name": "your_bucket"
}
}
```
3. **Restart the application**
**Migration Script (Optional):**
If you want to migrate existing local files to B2, create a simple script:
```python
#!/usr/bin/env python3
import os
from pathlib import Path
from src.storage import LocalStorageBackend, B2StorageBackend
# Initialize both backends
local = LocalStorageBackend("storage")
b2 = B2StorageBackend("key_id", "key", "bucket_name")
# Migrate files
for file_path in local.list_files():
print(f"Migrating: {file_path}")
content = local.download_file(file_path)
b2.upload_file(content, file_path)
print(f"✅ Migrated: {file_path}")
```
## Directory Structure
### Local Storage Structure
```
storage/
├── files/
│ ├── abc123.jpg
│ ├── def456.pdf
│ └── ...
└── pastes/
├── xyz789.txt
├── uvw012.txt
└── ...
```
### B2 Storage Structure
```
bucket/
├── files/
│ ├── abc123.jpg
│ ├── def456.pdf
│ └── ...
└── pastes/
├── xyz789.txt
├── uvw012.txt
└── ...
```
## Performance Considerations
### Local Storage
- **Pros:** Very fast access, no network latency
- **Cons:** Limited by disk I/O, single point of failure
### B2 Storage
- **Pros:** Unlimited capacity, built-in redundancy, CDN capabilities
- **Cons:** Network latency, dependent on internet connection
## Security
### Local Storage
- Files are stored with filesystem permissions
- Secure as your server's filesystem
- Access controlled by Sharey application
### B2 Storage
- Files stored with B2's encryption at rest
- Access controlled by application keys
- Private bucket - files not publicly accessible
- All access proxied through Sharey application
## Troubleshooting
### Common Issues
**"Storage backend not initialized"**
- Check your configuration syntax
- Verify credentials (for B2)
- Run `python test_storage.py`
**"B2 authentication failed"**
- Verify your application key ID and key
- Check bucket name spelling
- Ensure key has read/write permissions
**"Permission denied" (Local storage)**
- Check filesystem permissions on storage directory
- Ensure Sharey has write access to the path
**"File not found"**
- Check if storage backend was changed without migration
- Verify file was uploaded successfully
### Debug Commands
```bash
# Test storage backend
python test_storage.py
# Validate configuration
python src/config_util.py validate
# Check application logs
tail -f logs/sharey.log
# Test B2 connection (if using B2)
python tests/test_b2.py
```
## Best Practices
1. **Production:** Use B2 for scalability and reliability
2. **Development:** Local storage is fine for testing
3. **Backup:** Consider periodic backups regardless of backend
4. **Monitoring:** Monitor disk space (local) or B2 costs (cloud)
5. **Security:** Use environment variables for sensitive credentials in production
## Cost Considerations
### Local Storage
- **Cost:** Server disk space and bandwidth
- **Scaling:** Limited by hardware
### B2 Storage
- **Storage:** $0.005/GB/month
- **Download:** $0.01/GB
- **API calls:** Mostly free (10,000 free per day)
- **Scaling:** Pay as you grow
For most small to medium deployments, B2 costs are minimal (under $5/month for moderate usage).

BIN
expiry.db Normal file

Binary file not shown.

306
expiry_db.py Normal file
View File

@@ -0,0 +1,306 @@
#!/usr/bin/env python3
"""
Database for tracking file expiry in Sharey
Simple SQLite database to store file paths and their expiry times
"""
import sqlite3
import os
from datetime import datetime
from pathlib import Path
class ExpiryDatabase:
"""Manages file expiry tracking database"""
def __init__(self, db_path="expiry.db"):
self.db_path = db_path
self.init_database()
def init_database(self):
"""Initialize the expiry database with required tables"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# Create expiry table
cursor.execute('''
CREATE TABLE IF NOT EXISTS file_expiry (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT UNIQUE NOT NULL,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL,
storage_backend TEXT NOT NULL
)
''')
# Create URL shortener table
cursor.execute('''
CREATE TABLE IF NOT EXISTS url_redirects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
short_code TEXT UNIQUE NOT NULL,
target_url TEXT NOT NULL,
created_at TEXT NOT NULL,
expires_at TEXT,
click_count INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
created_by_ip TEXT,
notes TEXT
)
''')
conn.commit()
conn.close()
print(f"✅ Expiry database initialized: {self.db_path}")
def add_file(self, file_path: str, expires_at: str, storage_backend: str = "b2"):
"""Add a file with expiry time to the database"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
created_at = datetime.utcnow().isoformat() + 'Z'
cursor.execute('''
INSERT OR REPLACE INTO file_expiry
(file_path, expires_at, created_at, storage_backend)
VALUES (?, ?, ?, ?)
''', (file_path, expires_at, created_at, storage_backend))
conn.commit()
conn.close()
print(f"📝 Added expiry tracking: {file_path} expires at {expires_at}")
return True
except Exception as e:
print(f"❌ Failed to add expiry tracking for {file_path}: {e}")
return False
def get_expired_files(self) -> list:
"""Get list of files that have expired"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
current_time = datetime.utcnow().isoformat() + 'Z'
cursor.execute('''
SELECT file_path, expires_at, storage_backend
FROM file_expiry
WHERE expires_at <= ?
ORDER BY expires_at ASC
''', (current_time,))
expired_files = cursor.fetchall()
conn.close()
return [
{
'file_path': row[0],
'expires_at': row[1],
'storage_backend': row[2]
}
for row in expired_files
]
except Exception as e:
print(f"❌ Failed to get expired files: {e}")
return []
def remove_file(self, file_path: str):
"""Remove a file from expiry tracking (after deletion)"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('DELETE FROM file_expiry WHERE file_path = ?', (file_path,))
conn.commit()
conn.close()
print(f"🗑️ Removed from expiry tracking: {file_path}")
return True
except Exception as e:
print(f"❌ Failed to remove expiry tracking for {file_path}: {e}")
return False
def get_all_files(self) -> list:
"""Get all files in expiry tracking"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
SELECT file_path, expires_at, created_at, storage_backend
FROM file_expiry
ORDER BY expires_at ASC
''')
all_files = cursor.fetchall()
conn.close()
return [
{
'file_path': row[0],
'expires_at': row[1],
'created_at': row[2],
'storage_backend': row[3]
}
for row in all_files
]
except Exception as e:
print(f"❌ Failed to get all files: {e}")
return []
def get_stats(self) -> dict:
"""Get database statistics"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# Total files
cursor.execute('SELECT COUNT(*) FROM file_expiry')
total_files = cursor.fetchone()[0]
# Expired files
current_time = datetime.utcnow().isoformat() + 'Z'
cursor.execute('SELECT COUNT(*) FROM file_expiry WHERE expires_at <= ?', (current_time,))
expired_files = cursor.fetchone()[0]
# Files by storage backend
cursor.execute('SELECT storage_backend, COUNT(*) FROM file_expiry GROUP BY storage_backend')
by_backend = dict(cursor.fetchall())
conn.close()
return {
'total_files': total_files,
'expired_files': expired_files,
'active_files': total_files - expired_files,
'by_backend': by_backend
}
except Exception as e:
print(f"❌ Failed to get stats: {e}")
return {}
# URL Shortener Methods
def add_redirect(self, short_code, target_url, expires_at=None, created_by_ip=None, notes=None):
"""Add a URL redirect"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
created_at = datetime.utcnow().isoformat() + 'Z'
cursor.execute('''
INSERT INTO url_redirects
(short_code, target_url, created_at, expires_at, created_by_ip, notes)
VALUES (?, ?, ?, ?, ?, ?)
''', (short_code, target_url, created_at, expires_at, created_by_ip, notes))
conn.commit()
conn.close()
print(f"✅ Added redirect: {short_code} -> {target_url}")
return True
except sqlite3.IntegrityError:
print(f"❌ Short code already exists: {short_code}")
return False
except Exception as e:
print(f"❌ Failed to add redirect: {e}")
return False
def get_redirect(self, short_code):
"""Get redirect target URL and increment click count"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# Check if redirect exists and is active
cursor.execute('''
SELECT target_url, expires_at, is_active
FROM url_redirects
WHERE short_code = ? AND is_active = 1
''', (short_code,))
result = cursor.fetchone()
if not result:
conn.close()
return None
target_url, expires_at, is_active = result
# Check if expired
if expires_at:
current_time = datetime.utcnow().isoformat() + 'Z'
if expires_at <= current_time:
conn.close()
return None
# Increment click count
cursor.execute('''
UPDATE url_redirects
SET click_count = click_count + 1
WHERE short_code = ?
''', (short_code,))
conn.commit()
conn.close()
return target_url
except Exception as e:
print(f"❌ Failed to get redirect: {e}")
return None
def disable_redirect(self, short_code):
"""Disable a redirect (for abuse/takedown)"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
UPDATE url_redirects
SET is_active = 0
WHERE short_code = ?
''', (short_code,))
rows_affected = cursor.rowcount
conn.commit()
conn.close()
if rows_affected > 0:
print(f"✅ Disabled redirect: {short_code}")
return True
else:
print(f"❌ Redirect not found: {short_code}")
return False
except Exception as e:
print(f"❌ Failed to disable redirect: {e}")
return False
def list_redirects(self, limit=100):
"""List recent redirects for admin purposes"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
SELECT short_code, target_url, created_at, expires_at,
click_count, is_active, created_by_ip
FROM url_redirects
ORDER BY created_at DESC
LIMIT ?
''', (limit,))
redirects = []
for row in cursor.fetchall():
redirects.append({
'short_code': row[0],
'target_url': row[1],
'created_at': row[2],
'expires_at': row[3],
'click_count': row[4],
'is_active': bool(row[5]),
'created_by_ip': row[6]
})
conn.close()
return redirects
except Exception as e:
print(f"❌ Failed to list redirects: {e}")
return []

70
gunicorn.conf.py Normal file
View File

@@ -0,0 +1,70 @@
# Gunicorn configuration file for Sharey
# Save as: gunicorn.conf.py
import multiprocessing
import os
# Server socket
bind = "0.0.0.0:8866"
backlog = 2048
# Worker processes
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2
# Restart workers after this many requests, to help prevent memory leaks
max_requests = 1000
max_requests_jitter = 50
# Logging
accesslog = "logs/gunicorn_access.log"
errorlog = "logs/gunicorn_error.log"
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
# Process naming
proc_name = "sharey"
# Server mechanics
daemon = False
pidfile = "logs/gunicorn.pid"
user = None
group = None
tmp_upload_dir = None
# SSL (uncomment if you have SSL certificates)
# keyfile = "/path/to/keyfile"
# certfile = "/path/to/certfile"
# Security
limit_request_line = 4096
limit_request_fields = 100
limit_request_field_size = 8190
# Performance tuning
preload_app = True
enable_stdio_inheritance = True
# Worker timeout for file uploads (increase for large files)
graceful_timeout = 120
def when_ready(server):
server.log.info("Sharey server is ready. Listening on: %s", server.address)
def worker_int(worker):
worker.log.info("worker received INT or QUIT signal")
def pre_fork(server, worker):
server.log.info("Worker spawned (pid: %s)", worker.pid)
def post_fork(server, worker):
server.log.info("Worker spawned (pid: %s)", worker.pid)
def post_worker_init(worker):
worker.log.info("Worker initialized (pid: %s)", worker.pid)
def worker_abort(worker):
worker.log.info("Worker received SIGABRT signal")

122
horizontal-layout.js Normal file
View File

@@ -0,0 +1,122 @@
// Horizontal bar layout for file results
// Replace the entire displayUploadedFiles function with this:
function displayUploadedFiles(results) {
console.log('📁 Displaying file results:', results);
result.innerHTML = '';
const header = document.createElement('div');
header.innerHTML = '<h3>📁 Files Uploaded</h3>';
result.appendChild(header);
results.forEach((fileResult, index) => {
const fileContainer = document.createElement('div');
fileContainer.style.cssText = `
margin-bottom: 15px;
padding: 15px;
border: 2px solid #4CAF50;
border-radius: 8px;
background-color: #f0f8f0;
display: flex;
align-items: center;
gap: 15px;
flex-wrap: wrap;
`;
// Left: File info
const fileInfo = document.createElement('div');
fileInfo.style.cssText = `
flex: 1;
min-width: 180px;
`;
const fileName = document.createElement('div');
fileName.innerHTML = `<strong>📄 ${fileResult.file.name}</strong>`;
fileName.style.marginBottom = '5px';
fileInfo.appendChild(fileName);
const fileSize = document.createElement('div');
fileSize.innerHTML = `📊 ${(fileResult.originalSize / 1024).toFixed(1)} KB`;
fileSize.style.cssText = `
font-size: 14px;
color: #666;
`;
fileInfo.appendChild(fileSize);
fileContainer.appendChild(fileInfo);
// Middle: Action buttons
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
gap: 8px;
flex-wrap: wrap;
`;
// Share button
const shareButton = document.createElement('button');
shareButton.textContent = '🔗 Copy';
shareButton.className = 'share-button';
shareButton.style.cssText = `padding: 6px 10px; font-size: 13px;`;
shareButton.addEventListener('click', () => {
navigator.clipboard.writeText(fileResult.url).then(() => {
shareButton.textContent = '✅ Copied!';
setTimeout(() => {
shareButton.textContent = '🔗 Copy';
}, 2000);
});
});
buttonContainer.appendChild(shareButton);
// View button
const viewButton = document.createElement('button');
viewButton.textContent = '👁️ View';
viewButton.className = 'view-button';
viewButton.style.cssText = `padding: 6px 10px; font-size: 13px;`;
viewButton.addEventListener('click', () => {
window.open(fileResult.url, '_blank');
});
buttonContainer.appendChild(viewButton);
// QR Code button
const qrButton = document.createElement('button');
qrButton.textContent = '📱 QR';
qrButton.className = 'qr-button';
qrButton.style.cssText = `
background-color: #9C27B0;
color: white;
padding: 6px 10px;
font-size: 13px;
`;
qrButton.addEventListener('click', () => {
showQRCode(fileResult.url, fileResult.file.name);
});
buttonContainer.appendChild(qrButton);
fileContainer.appendChild(buttonContainer);
// Right: Image thumbnail (if it's an image)
if (isImageFile(fileResult.file)) {
const thumbnail = document.createElement('img');
thumbnail.src = fileResult.url;
thumbnail.alt = fileResult.file.name;
thumbnail.style.cssText = `
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 6px;
border: 1px solid #ddd;
flex-shrink: 0;
`;
thumbnail.onerror = () => {
thumbnail.style.display = 'none';
};
fileContainer.appendChild(thumbnail);
}
result.appendChild(fileContainer);
});
}

107
production.py Normal file
View File

@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Production runner for Sharey using Gunicorn
"""
import sys
import os
import subprocess
from pathlib import Path
def check_gunicorn():
"""Check if gunicorn is installed"""
try:
import gunicorn
return True
except ImportError:
return False
def install_gunicorn():
"""Install gunicorn"""
print("📦 Installing gunicorn...")
try:
subprocess.check_call([sys.executable, "-m", "pip", "install", "gunicorn"])
print("✅ Gunicorn installed successfully")
return True
except subprocess.CalledProcessError:
print("❌ Failed to install gunicorn")
return False
def start_production_server():
"""Start Sharey with Gunicorn"""
# Add src directory to Python path
src_path = Path(__file__).parent / "src"
if str(src_path) not in sys.path:
sys.path.insert(0, str(src_path))
# Check if gunicorn is available
if not check_gunicorn():
print("⚠️ Gunicorn not found")
if input("📦 Install gunicorn? (y/n): ").lower().startswith('y'):
if not install_gunicorn():
return False
else:
print("❌ Cannot start production server without gunicorn")
return False
print("🚀 Starting Sharey with Gunicorn (Production Mode)")
print("=" * 60)
# Load configuration
try:
from config import config
host = config.get('flask.host', '0.0.0.0')
port = config.get('flask.port', 8866)
print(f"🌐 Host: {host}")
print(f"🔌 Port: {port}")
print(f"📁 Working directory: {Path.cwd()}")
print("=" * 60)
except Exception as e:
print(f"❌ Error loading configuration: {e}")
return False
# Gunicorn command arguments
gunicorn_args = [
sys.executable, "-m", "gunicorn",
"--bind", f"{host}:{port}",
"--workers", "4",
"--worker-class", "sync",
"--timeout", "120",
"--max-requests", "1000",
"--max-requests-jitter", "100",
"--access-logfile", "-",
"--error-logfile", "-",
"--log-level", "info",
"--pythonpath", str(src_path),
"wsgi:app"
]
try:
print("🎯 Starting server...")
print("Press Ctrl+C to stop")
print("=" * 60)
subprocess.run(gunicorn_args)
except KeyboardInterrupt:
print("\n👋 Server stopped by user")
except Exception as e:
print(f"❌ Error starting server: {e}")
return False
return True
def main():
"""Main entry point"""
print("🏭 Sharey Production Server")
print("=" * 40)
if start_production_server():
print("✅ Server stopped gracefully")
else:
print("❌ Server failed to start")
sys.exit(1)
if __name__ == "__main__":
main()

5
requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
Flask==2.3.3
b2sdk>=2.9.0
python-dotenv==1.0.0
setuptools<75
gunicorn>=21.2.0

25
scripts/clean.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
# Clean up temporary files and caches
echo "🧹 Cleaning up Sharey project..."
# Remove Python cache
echo "🐍 Removing Python cache..."
find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
find . -name "*.pyc" -delete 2>/dev/null || true
find . -name "*.pyo" -delete 2>/dev/null || true
# Remove temporary files
echo "🗑️ Removing temporary files..."
rm -f *.tmp *.temp debug_*.html test_*.ppm
# Remove old log files (keep recent ones)
echo "📜 Cleaning old logs..."
find logs/ -name "*.log" -mtime +7 -delete 2>/dev/null || true
# Remove backup files
echo "💾 Removing backup files..."
rm -f *.backup *.bak config.json.backup.*
echo "✅ Cleanup complete!"

35
scripts/config.json Normal file
View File

@@ -0,0 +1,35 @@
{
"b2": {
"application_key_id": "your_key_id_here",
"application_key": "your_application_key_here",
"bucket_name": "your_bucket_name_here"
},
"flask": {
"host": "127.0.0.1",
"port": 8866,
"debug": true
},
"upload": {
"max_file_size_mb": 100,
"allowed_extensions": [
".jpg",
".jpeg",
".png",
".gif",
".pdf",
".txt",
".doc",
".docx",
".zip",
".mp4",
".mp3"
]
},
"paste": {
"max_length": 1000000
},
"security": {
"rate_limit_enabled": false,
"max_uploads_per_hour": 50
}
}

450
scripts/migrate.py Normal file
View File

@@ -0,0 +1,450 @@
#!/usr/bin/env python3
"""
Sharey Local-to-B2 Migration Script
This script migrates existing local files and pastes to Backblaze B2
while preserving their original IDs and structure.
Sharey Naming Conventions:
- Files: 6-char random ID + original extension (e.g., abc123.jpg)
- Pastes: 6-char UUID prefix + .txt extension (e.g., def456.txt)
- B2 Structure: files/{file_id} and pastes/{paste_id}.txt
"""
import os
import sys
import mimetypes
from pathlib import Path
from typing import Dict, List, Tuple
import json
from datetime import datetime
try:
from b2sdk.v2 import InMemoryAccountInfo, B2Api
from config import config
except ImportError as e:
print(f"❌ Missing dependencies: {e}")
print("💡 Make sure you're running this script in the same environment as your Sharey app")
print("💡 Run: pip install -r requirements.txt")
sys.exit(1)
class ShareyMigrator:
"""Handles migration of local Sharey files to B2"""
def __init__(self):
self.b2_api = None
self.bucket = None
self.stats = {
'files_migrated': 0,
'pastes_migrated': 0,
'files_skipped': 0,
'pastes_skipped': 0,
'errors': 0,
'total_size': 0
}
self.migration_log = []
def initialize_b2(self) -> bool:
"""Initialize B2 connection"""
print("🔧 Initializing B2 connection...")
# Validate B2 configuration
if not config.validate_b2_config():
print("❌ Invalid B2 configuration. Please check your config.json")
return False
try:
b2_config = config.get_b2_config()
print(f"📋 Target bucket: {b2_config['bucket_name']}")
info = InMemoryAccountInfo()
self.b2_api = B2Api(info)
self.b2_api.authorize_account("production", b2_config['key_id'], b2_config['key'])
self.bucket = self.b2_api.get_bucket_by_name(b2_config['bucket_name'])
print("✅ B2 connection established")
return True
except Exception as e:
print(f"❌ Failed to connect to B2: {e}")
return False
def scan_local_directories(self, base_path: str = ".") -> Tuple[List[str], List[str]]:
"""Scan for local uploads and pastes directories"""
print(f"🔍 Scanning for local files in: {os.path.abspath(base_path)}")
uploads_dir = os.path.join(base_path, "uploads")
pastes_dir = os.path.join(base_path, "pastes")
file_paths = []
paste_paths = []
# Scan uploads directory
if os.path.exists(uploads_dir):
print(f"📁 Found uploads directory: {uploads_dir}")
for root, dirs, files in os.walk(uploads_dir):
for file in files:
# Skip hidden files, metadata files, and any Sharey system files
if (not file.startswith('.') and
not file.endswith('.sharey-meta') and
'.sharey-meta' not in file):
file_paths.append(os.path.join(root, file))
print(f" Found {len(file_paths)} files (skipped .sharey-meta files)")
else:
print(f"⚠️ No uploads directory found at: {uploads_dir}")
# Scan pastes directory
if os.path.exists(pastes_dir):
print(f"📝 Found pastes directory: {pastes_dir}")
for root, dirs, files in os.walk(pastes_dir):
for file in files:
if not file.startswith('.'): # Skip hidden files
paste_paths.append(os.path.join(root, file))
print(f" Found {len(paste_paths)} pastes")
else:
print(f"⚠️ No pastes directory found at: {pastes_dir}")
return file_paths, paste_paths
def extract_id_from_path(self, file_path: str, base_dir: str) -> str:
"""Extract the file ID from the file path"""
# Get relative path from base directory
rel_path = os.path.relpath(file_path, base_dir)
# Extract filename without extension for ID
filename = os.path.basename(rel_path)
file_id = os.path.splitext(filename)[0]
# Validate ID format (should be 6 characters for Sharey)
if len(file_id) != 6:
print(f"⚠️ Warning: {filename} has non-standard ID length ({len(file_id)} chars, expected 6)")
return file_id
def file_exists_in_b2(self, b2_path: str) -> bool:
"""Check if a file already exists in B2"""
try:
# Try different methods depending on B2 SDK version
if hasattr(self.bucket, 'get_file_info_by_name'):
file_info = self.bucket.get_file_info_by_name(b2_path)
return True
elif hasattr(self.bucket, 'ls'):
for file_version, _ in self.bucket.ls(b2_path, recursive=False):
if file_version.file_name == b2_path:
return True
return False
else:
# Fallback - assume doesn't exist to avoid skipping
return False
except:
return False
def migrate_file(self, local_path: str, uploads_dir: str, dry_run: bool = False) -> bool:
"""Migrate a single file to B2"""
try:
# Extract file ID and determine B2 path
file_id = self.extract_id_from_path(local_path, uploads_dir)
file_extension = os.path.splitext(local_path)[1]
b2_path = f"files/{file_id}{file_extension}"
# Check if file already exists in B2
if self.file_exists_in_b2(b2_path):
print(f"⏭️ Skipping {file_id} (already exists in B2)")
self.stats['files_skipped'] += 1
return True
# Get file info
file_size = os.path.getsize(local_path)
content_type = mimetypes.guess_type(local_path)[0] or 'application/octet-stream'
print(f"📤 Uploading file: {file_id}{file_extension} ({file_size:,} bytes)")
if dry_run:
print(f" [DRY RUN] Would upload to: {b2_path}")
self.stats['files_migrated'] += 1
self.stats['total_size'] += file_size
return True
# Upload to B2 - try different methods for different SDK versions
with open(local_path, 'rb') as file_data:
data = file_data.read()
# Try different upload methods
try:
# Method 1: upload_bytes (newer SDK)
if hasattr(self.bucket, 'upload_bytes'):
file_info = self.bucket.upload_bytes(
data,
b2_path,
content_type=content_type
)
# Method 2: upload with file-like object (older SDK)
elif hasattr(self.bucket, 'upload_file'):
from io import BytesIO
file_obj = BytesIO(data)
file_info = self.bucket.upload_file(
file_obj,
b2_path,
content_type=content_type
)
# Method 3: upload with upload source (alternative)
elif hasattr(self.bucket, 'upload'):
from io import BytesIO
file_obj = BytesIO(data)
file_info = self.bucket.upload(
file_obj,
b2_path,
content_type=content_type
)
else:
raise Exception("No compatible upload method found in B2 SDK")
except Exception as upload_error:
raise Exception(f"Upload failed: {upload_error}")
self.stats['files_migrated'] += 1
self.stats['total_size'] += file_size
self.migration_log.append(f"FILE: {file_id}{file_extension} -> {b2_path}")
print(f" ✅ Uploaded successfully")
return True
except Exception as e:
print(f" ❌ Failed to upload {local_path}: {e}")
self.stats['errors'] += 1
self.migration_log.append(f"ERROR: {local_path} -> {e}")
return False
def migrate_paste(self, local_path: str, pastes_dir: str, dry_run: bool = False) -> bool:
"""Migrate a single paste to B2"""
try:
# Extract paste ID and determine B2 path
paste_id = self.extract_id_from_path(local_path, pastes_dir)
b2_path = f"pastes/{paste_id}.txt"
# Check if paste already exists in B2
if self.file_exists_in_b2(b2_path):
print(f"⏭️ Skipping paste {paste_id} (already exists in B2)")
self.stats['pastes_skipped'] += 1
return True
# Get paste info
file_size = os.path.getsize(local_path)
print(f"📝 Uploading paste: {paste_id} ({file_size:,} bytes)")
if dry_run:
print(f" [DRY RUN] Would upload to: {b2_path}")
self.stats['pastes_migrated'] += 1
self.stats['total_size'] += file_size
return True
# Read and upload paste content
with open(local_path, 'r', encoding='utf-8', errors='ignore') as file:
content = file.read()
# Upload to B2 as UTF-8 text - try different methods
data = content.encode('utf-8')
try:
# Method 1: upload_bytes (newer SDK)
if hasattr(self.bucket, 'upload_bytes'):
self.bucket.upload_bytes(
data,
b2_path,
content_type='text/plain; charset=utf-8'
)
# Method 2: upload with file-like object (older SDK)
elif hasattr(self.bucket, 'upload_file'):
from io import BytesIO
file_obj = BytesIO(data)
self.bucket.upload_file(
file_obj,
b2_path,
content_type='text/plain; charset=utf-8'
)
# Method 3: upload with upload source (alternative)
elif hasattr(self.bucket, 'upload'):
from io import BytesIO
file_obj = BytesIO(data)
self.bucket.upload(
file_obj,
b2_path,
content_type='text/plain; charset=utf-8'
)
else:
raise Exception("No compatible upload method found in B2 SDK")
except Exception as upload_error:
raise Exception(f"Upload failed: {upload_error}")
self.stats['pastes_migrated'] += 1
self.stats['total_size'] += file_size
self.migration_log.append(f"PASTE: {paste_id} -> {b2_path}")
print(f" ✅ Uploaded successfully")
return True
except Exception as e:
print(f" ❌ Failed to upload paste {local_path}: {e}")
self.stats['errors'] += 1
self.migration_log.append(f"ERROR: {local_path} -> {e}")
return False
def migrate_all(self, base_path: str = ".", dry_run: bool = False, skip_files: bool = False, skip_pastes: bool = False):
"""Migrate all local files and pastes to B2"""
if dry_run:
print("🧪 DRY RUN MODE - No files will actually be uploaded")
print(f"\n🚀 Starting migration from: {os.path.abspath(base_path)}")
print("=" * 60)
# Scan for local files
file_paths, paste_paths = self.scan_local_directories(base_path)
if not file_paths and not paste_paths:
print("❌ No files or pastes found to migrate")
return False
total_items = len(file_paths) + len(paste_paths)
print(f"\n📊 Migration Plan:")
print(f" Files to migrate: {len(file_paths)}")
print(f" Pastes to migrate: {len(paste_paths)}")
print(f" Total items: {total_items}")
if not dry_run:
confirm = input(f"\n❓ Proceed with migration? (y/N): ").strip().lower()
if confirm != 'y':
print("Migration cancelled")
return False
print(f"\n🔄 Starting migration...")
print("-" * 40)
# Migrate files
if file_paths and not skip_files:
print(f"\n📁 Migrating {len(file_paths)} files...")
uploads_dir = os.path.join(base_path, "uploads")
for i, file_path in enumerate(file_paths, 1):
print(f"[{i}/{len(file_paths)}] ", end="")
self.migrate_file(file_path, uploads_dir, dry_run)
# Migrate pastes
if paste_paths and not skip_pastes:
print(f"\n📝 Migrating {len(paste_paths)} pastes...")
pastes_dir = os.path.join(base_path, "pastes")
for i, paste_path in enumerate(paste_paths, 1):
print(f"[{i}/{len(paste_paths)}] ", end="")
self.migrate_paste(paste_path, pastes_dir, dry_run)
self.print_summary(dry_run)
self.save_migration_log()
return True
def print_summary(self, dry_run: bool = False):
"""Print migration summary"""
print("\n" + "=" * 60)
print("📊 MIGRATION SUMMARY")
print("=" * 60)
if dry_run:
print("🧪 DRY RUN RESULTS:")
print(f"✅ Files migrated: {self.stats['files_migrated']}")
print(f"✅ Pastes migrated: {self.stats['pastes_migrated']}")
print(f"⏭️ Files skipped: {self.stats['files_skipped']}")
print(f"⏭️ Pastes skipped: {self.stats['pastes_skipped']}")
print(f"❌ Errors: {self.stats['errors']}")
print(f"📦 Total data: {self.stats['total_size']:,} bytes ({self.stats['total_size'] / 1024 / 1024:.2f} MB)")
success_rate = ((self.stats['files_migrated'] + self.stats['pastes_migrated']) /
max(1, self.stats['files_migrated'] + self.stats['pastes_migrated'] + self.stats['errors'])) * 100
print(f"📈 Success rate: {success_rate:.1f}%")
if not dry_run and (self.stats['files_migrated'] > 0 or self.stats['pastes_migrated'] > 0):
print(f"\n🎉 Migration completed successfully!")
print(f"💡 Your files are now accessible via your Sharey B2 URLs")
def save_migration_log(self):
"""Save migration log to file"""
if not self.migration_log:
return
log_filename = f"migration_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
try:
with open(log_filename, 'w') as f:
f.write(f"Sharey B2 Migration Log\n")
f.write(f"Generated: {datetime.now().isoformat()}\n")
f.write(f"=" * 50 + "\n\n")
for entry in self.migration_log:
f.write(f"{entry}\n")
f.write(f"\n" + "=" * 50 + "\n")
f.write(f"SUMMARY:\n")
f.write(f"Files migrated: {self.stats['files_migrated']}\n")
f.write(f"Pastes migrated: {self.stats['pastes_migrated']}\n")
f.write(f"Files skipped: {self.stats['files_skipped']}\n")
f.write(f"Pastes skipped: {self.stats['pastes_skipped']}\n")
f.write(f"Errors: {self.stats['errors']}\n")
f.write(f"Total size: {self.stats['total_size']:,} bytes\n")
print(f"📄 Migration log saved to: {log_filename}")
except Exception as e:
print(f"⚠️ Failed to save migration log: {e}")
def main():
"""Main migration function"""
print("🚀 Sharey Local-to-B2 Migration Tool")
print("=" * 50)
# Parse command line arguments
import argparse
parser = argparse.ArgumentParser(description='Migrate local Sharey files to Backblaze B2')
parser.add_argument('--path', '-p', default='.', help='Path to Sharey directory (default: current directory)')
parser.add_argument('--dry-run', '-d', action='store_true', help='Perform a dry run without uploading')
parser.add_argument('--skip-files', action='store_true', help='Skip file migration')
parser.add_argument('--skip-pastes', action='store_true', help='Skip paste migration')
parser.add_argument('--force', '-f', action='store_true', help='Skip confirmation prompt')
args = parser.parse_args()
# Initialize migrator
migrator = ShareyMigrator()
# Initialize B2 connection
if not migrator.initialize_b2():
print("❌ Failed to initialize B2 connection")
sys.exit(1)
# Run migration
try:
success = migrator.migrate_all(
base_path=args.path,
dry_run=args.dry_run,
skip_files=args.skip_files,
skip_pastes=args.skip_pastes
)
if success:
print(f"\n💡 Next steps:")
print(f" 1. Test your Sharey app to ensure URLs work correctly")
print(f" 2. Consider backing up your local files before deletion")
print(f" 3. Update any hardcoded URLs to use the new B2 structure")
sys.exit(0)
else:
sys.exit(1)
except KeyboardInterrupt:
print(f"\n⏹️ Migration cancelled by user")
sys.exit(1)
except Exception as e:
print(f"\n❌ Migration failed: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""
Simple script to set admin password for Sharey
"""
import hashlib
import json
def set_admin_password(password):
"""Set admin password in config.json"""
try:
# Load current config
with open('config.json', 'r') as f:
config = json.load(f)
# Hash the password
password_hash = hashlib.sha256(password.encode()).hexdigest()
# Ensure admin section exists
if 'admin' not in config:
config['admin'] = {}
config['admin']['password_hash'] = password_hash
config['admin']['session_timeout_minutes'] = config['admin'].get('session_timeout_minutes', 30)
# Save config
with open('config.json', 'w') as f:
json.dump(config, f, indent=2)
print("✅ Admin password set successfully!")
print("💡 You can now access the admin panel at /admin")
return True
except Exception as e:
print(f"❌ Error setting admin password: {e}")
return False
if __name__ == "__main__":
import sys
if len(sys.argv) != 2:
print("Usage: python set_admin_password.py <password>")
print("Example: python set_admin_password.py mySecurePassword123")
sys.exit(1)
password = sys.argv[1]
if len(password) < 6:
print("❌ Password must be at least 6 characters long")
sys.exit(1)
set_admin_password(password)

280
scripts/setup.py Normal file
View File

@@ -0,0 +1,280 @@
#!/usr/bin/env python3
"""
Sharey B2 Setup Script
"""
import os
import sys
import subprocess
import shutil
def check_python():
"""Check if we have a compatible Python version"""
if sys.version_info < (3, 7):
print("❌ Python 3.7+ is required. Current version:", sys.version)
sys.exit(1)
print(f"✅ Python {sys.version.split()[0]} detected")
def create_env_file():
"""Create config.json file from template if it doesn't exist"""
if not os.path.exists('config.json'):
if os.path.exists('config.json.example'):
shutil.copy('config.json.example', 'config.json')
print("📄 Created config.json file from template")
print("\nPlease edit config.json with your Backblaze B2 credentials:")
print(" - b2.application_key_id: Your B2 application key ID")
print(" - b2.application_key: Your B2 application key")
print(" - b2.bucket_name: Your B2 bucket name")
print("\nYou can get these credentials from your Backblaze account:")
print(" 1. Go to https://secure.backblaze.com/app_keys.htm")
print(" 2. Create a new application key or use an existing one")
print(" 3. Create a bucket or use an existing one")
else:
print("❌ config.json.example not found. Creating basic template...")
basic_config = {
"b2": {
"application_key_id": "your_key_id_here",
"application_key": "your_application_key_here",
"bucket_name": "your_bucket_name_here"
},
"flask": {
"host": "127.0.0.1",
"port": 8866,
"debug": True
},
"upload": {
"max_file_size_mb": 100,
"allowed_extensions": [".jpg", ".jpeg", ".png", ".gif", ".pdf", ".txt", ".doc", ".docx", ".zip", ".mp4", ".mp3"]
},
"paste": {
"max_length": 1000000
},
"security": {
"rate_limit_enabled": False,
"max_uploads_per_hour": 50
}
}
import json
with open('config.json', 'w') as f:
json.dump(basic_config, f, indent=2)
print("📄 Created basic config.json template")
else:
print("📄 config.json file already exists. Please ensure it has the correct B2 credentials.")
# Also create .env for backwards compatibility if it doesn't exist
if not os.path.exists('.env'):
if os.path.exists('.env.example'):
shutil.copy('.env.example', '.env')
print("📄 Also created .env file for backwards compatibility")
def install_dependencies():
"""Install Python dependencies"""
print("\n📦 Installing Python dependencies...")
if not os.path.exists('requirements.txt'):
print("❌ requirements.txt not found!")
return False
# Check if we're in a virtual environment
in_venv = (hasattr(sys, 'real_prefix') or
(hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix))
if not in_venv:
print("⚠️ Not in a virtual environment")
print("Creating virtual environment...")
try:
subprocess.run([sys.executable, '-m', 'venv', 'venv'], check=True)
print("✅ Virtual environment created")
# Determine the correct pip path
if os.name == 'nt': # Windows
pip_path = os.path.join('venv', 'Scripts', 'pip')
python_path = os.path.join('venv', 'Scripts', 'python')
else: # Unix-like
pip_path = os.path.join('venv', 'bin', 'pip')
python_path = os.path.join('venv', 'bin', 'python')
# Install dependencies in virtual environment
subprocess.run([pip_path, 'install', '-r', 'requirements.txt'], check=True)
print("✅ Dependencies installed in virtual environment")
print(f"💡 To activate the virtual environment:")
if os.name == 'nt':
print(" venv\\Scripts\\activate")
else:
print(" source venv/bin/activate")
print(f"💡 Then run the app with: {python_path} app.py")
return True
except subprocess.CalledProcessError as e:
print(f"❌ Failed to create virtual environment or install dependencies: {e}")
return False
else:
print("✅ In virtual environment")
try:
subprocess.run([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt'],
check=True, capture_output=True, text=True)
print("✅ Dependencies installed successfully")
return True
except subprocess.CalledProcessError as e:
print(f"❌ Failed to install dependencies: {e}")
print("Error output:", e.stderr)
return False
def test_imports():
"""Test if required modules can be imported"""
print("\n🧪 Testing imports...")
# Check if we're in a virtual environment or if venv was created
in_venv = (hasattr(sys, 'real_prefix') or
(hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix))
if not in_venv and os.path.exists('venv'):
print("💡 Skipping import test - using virtual environment")
print(" Imports will be tested when you activate the virtual environment")
return True
required_modules = ['flask', 'b2sdk', 'dotenv']
missing_modules = []
for module in required_modules:
try:
__import__(module)
print(f"{module}")
except ImportError:
print(f"{module}")
missing_modules.append(module)
if missing_modules:
print(f"\n❌ Missing modules: {', '.join(missing_modules)}")
if not in_venv:
print("💡 This is expected if using a virtual environment")
return True
else:
print("Try running: pip install -r requirements.txt")
return False
print("✅ All required modules available")
return True
def check_b2_config():
"""Check if B2 configuration looks valid"""
print("\n🔧 Checking B2 configuration...")
# Check config.json first
if os.path.exists('config.json'):
try:
import json
with open('config.json', 'r') as f:
config_data = json.load(f)
b2_config = config_data.get('b2', {})
required_keys = ['application_key_id', 'application_key', 'bucket_name']
invalid_values = ['your_key_id_here', 'your_application_key_here', 'your_bucket_name_here', '']
missing_keys = []
for key in required_keys:
value = b2_config.get(key)
if not value or value in invalid_values:
missing_keys.append(f'b2.{key}')
if missing_keys:
print(f"❌ Please configure these B2 settings in config.json: {', '.join(missing_keys)}")
return False
print("✅ B2 configuration looks valid in config.json")
return True
except (json.JSONDecodeError, KeyError) as e:
print(f"❌ Error reading config.json: {e}")
return False
# Fall back to .env file
elif os.path.exists('.env'):
b2_config = {}
with open('.env', 'r') as f:
for line in f:
line = line.strip()
if '=' in line and not line.startswith('#'):
key, value = line.split('=', 1)
b2_config[key] = value
required_keys = ['B2_APPLICATION_KEY_ID', 'B2_APPLICATION_KEY', 'B2_BUCKET_NAME']
missing_keys = []
for key in required_keys:
if key not in b2_config or b2_config[key] in ['', 'your_key_id_here', 'your_application_key_here', 'your_bucket_name_here']:
missing_keys.append(key)
if missing_keys:
print(f"❌ Please configure these B2 settings in .env: {', '.join(missing_keys)}")
return False
print("✅ B2 configuration looks valid in .env")
return True
else:
print("❌ No configuration file found (config.json or .env)")
return False
def main():
"""Main setup function"""
print("🚀 Setting up Sharey with Backblaze B2 Storage...\n")
# Check Python version
check_python()
# Create .env file
create_env_file()
# Install dependencies
if not install_dependencies():
print("\n❌ Setup failed during dependency installation")
sys.exit(1)
# Test imports
if not test_imports():
print("\n❌ Setup failed: missing required modules")
sys.exit(1)
# Check B2 configuration
config_valid = check_b2_config()
print("\n" + "="*60)
print("🎉 Setup complete!")
print("="*60)
# Check if we're in a virtual environment
in_venv = (hasattr(sys, 'real_prefix') or
(hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix))
if not in_venv and os.path.exists('venv'):
print("\n💡 Virtual environment created!")
print("To use Sharey:")
if os.name == 'nt': # Windows
print(" 1. Activate virtual environment: venv\\Scripts\\activate")
print(" 2. Edit config.json with your B2 credentials")
print(" 3. Test B2 connection: venv\\Scripts\\python test_b2.py")
print(" 4. Run the app: venv\\Scripts\\python app.py")
else: # Unix-like
print(" 1. Activate virtual environment: source venv/bin/activate")
print(" 2. Edit config.json with your B2 credentials")
print(" 3. Test B2 connection: python test_b2.py")
print(" 4. Run the app: python app.py")
else:
if not config_valid:
print("\n⚠️ Next steps:")
print(" 1. Edit config.json with your B2 credentials")
print(" 2. Run: python test_b2.py (to test B2 connection)")
print(" 3. Run: python app.py (to start the application)")
else:
print("\n✅ Next steps:")
print(" 1. Run: python test_b2.py (to test B2 connection)")
print(" 2. Run: python app.py (to start the application)")
print("\n📋 Notes:")
print(" - Make sure your B2 bucket allows public downloads")
print(" - Application will be available at http://127.0.0.1:8866")
print(" - Check DEPLOYMENT.md for production deployment guide")
if __name__ == "__main__":
main()

51
set_admin_password.py Normal file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python3
"""
Simple script to set admin password for Sharey
"""
import hashlib
import json
def set_admin_password(password):
"""Set admin password in config.json"""
try:
# Load current config
with open('config.json', 'r') as f:
config = json.load(f)
# Hash the password
password_hash = hashlib.sha256(password.encode()).hexdigest()
# Ensure admin section exists
if 'admin' not in config:
config['admin'] = {}
config['admin']['password_hash'] = password_hash
config['admin']['session_timeout_minutes'] = config['admin'].get('session_timeout_minutes', 30)
# Save config
with open('config.json', 'w') as f:
json.dump(config, f, indent=2)
print("✅ Admin password set successfully!")
print("💡 You can now access the admin panel at /admin")
return True
except Exception as e:
print(f"❌ Error setting admin password: {e}")
return False
if __name__ == "__main__":
import sys
if len(sys.argv) != 2:
print("Usage: python set_admin_password.py <password>")
print("Example: python set_admin_password.py mySecurePassword123")
sys.exit(1)
password = sys.argv[1]
if len(password) < 6:
print("❌ Password must be at least 6 characters long")
sys.exit(1)
set_admin_password(password)

57
sharey Executable file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""
Sharey - Production Entry Point
Simple Python-based entry point for the Sharey file sharing platform
"""
import sys
import os
import subprocess
import argparse
from pathlib import Path
def main():
parser = argparse.ArgumentParser(description='Sharey File Sharing Platform')
parser.add_argument('command', nargs='?', default='start',
choices=['start', 'stop', 'restart', 'status'],
help='Command to execute (default: start)')
parser.add_argument('--port', '-p', type=int, default=8000,
help='Port to run on (default: 8000)')
parser.add_argument('--host', default='0.0.0.0',
help='Host to bind to (default: 0.0.0.0)')
args = parser.parse_args()
script_dir = Path(__file__).parent
os.chdir(script_dir)
if args.command == 'start':
print("🚀 Starting Sharey production server...")
subprocess.run([sys.executable, 'production.py'], check=True)
elif args.command == 'stop':
print("🛑 Stopping Sharey server...")
# Kill gunicorn processes
try:
subprocess.run(['pkill', '-f', 'gunicorn.*sharey'], check=False)
print("✅ Server stopped")
except:
print("❌ Error stopping server")
elif args.command == 'restart':
print("🔄 Restarting Sharey server...")
subprocess.run(['pkill', '-f', 'gunicorn.*sharey'], check=False)
subprocess.run([sys.executable, 'production.py'], check=True)
elif args.command == 'status':
print("📊 Checking Sharey server status...")
result = subprocess.run(['pgrep', '-f', 'gunicorn.*sharey'],
capture_output=True, text=True)
if result.stdout.strip():
print("✅ Server is running")
print(f"PIDs: {result.stdout.strip()}")
else:
print("❌ Server is not running")
if __name__ == '__main__':
main()

303
sharey.py Normal file
View File

@@ -0,0 +1,303 @@
#!/usr/bin/env python3
"""
Sharey - Command Line Utility
Cross-platform management script for Sharey application
"""
import sys
import os
import subprocess
import argparse
from pathlib import Path
import json
import time
class ShareyManager:
def __init__(self):
self.script_dir = Path(__file__).parent
os.chdir(self.script_dir)
def print_banner(self):
"""Print Sharey banner"""
print("🚀 Sharey - Command Line Utility")
print("=" * 40)
def start_app(self, dev_mode=False, production_mode=False):
"""Start the application"""
if production_mode:
print("🏭 Starting Sharey in production mode with Gunicorn...")
return subprocess.run([sys.executable, "production.py"])
elif dev_mode:
print("🔧 Starting Sharey in development mode...")
if Path("dev.py").exists():
return subprocess.run([sys.executable, "dev.py"])
else:
os.environ['FLASK_ENV'] = 'development'
return subprocess.run([sys.executable, "run.py"])
else:
print("🚀 Starting Sharey with full checks...")
return subprocess.run([sys.executable, "run.py"])
def setup_environment(self):
"""Set up development environment"""
print("📦 Setting up development environment...")
if Path("dev-setup.sh").exists():
return subprocess.run(["./dev-setup.sh"])
else:
# Fallback Python setup
print("Running Python setup...")
# Create venv if it doesn't exist
if not Path(".venv").exists():
print("Creating virtual environment...")
subprocess.run([sys.executable, "-m", "venv", ".venv"])
# Install dependencies
if Path("requirements.txt").exists():
print("Installing dependencies...")
pip_path = ".venv/bin/pip"
subprocess.run([pip_path, "install", "-r", "requirements.txt"])
# Copy config if needed
if not Path("config.json").exists() and Path("config.json.example").exists():
print("Creating config.json from example...")
import shutil
shutil.copy2("config.json.example", "config.json")
print("✏️ Please edit config.json with your settings!")
def clean_project(self):
"""Clean up temporary files"""
print("🧹 Cleaning project...")
if Path("scripts/clean.sh").exists():
subprocess.run(["./scripts/clean.sh"])
else:
# Python cleanup
print("Removing Python cache...")
self._remove_pycache()
print("Removing temporary files...")
temp_patterns = ["*.tmp", "*.temp", "debug_*.html", "test_*.ppm"]
for pattern in temp_patterns:
for file in Path(".").glob(pattern):
file.unlink(missing_ok=True)
print("Cleaning old logs...")
logs_dir = Path("logs")
if logs_dir.exists():
for log_file in logs_dir.glob("*.log"):
# Remove logs older than 7 days
if time.time() - log_file.stat().st_mtime > 7 * 24 * 3600:
log_file.unlink(missing_ok=True)
print("Removing backup files...")
for backup in Path(".").glob("*.backup"):
backup.unlink(missing_ok=True)
for backup in Path(".").glob("*.bak"):
backup.unlink(missing_ok=True)
def _remove_pycache(self):
"""Remove Python cache directories"""
for pycache in Path(".").rglob("__pycache__"):
if pycache.is_dir():
import shutil
shutil.rmtree(pycache, ignore_errors=True)
for pyc in Path(".").rglob("*.pyc"):
pyc.unlink(missing_ok=True)
def run_tests(self):
"""Run tests"""
print("🧪 Running tests...")
tests_dir = Path("tests")
if tests_dir.exists():
os.chdir(tests_dir)
result = subprocess.run([sys.executable, "-m", "pytest", "-v"])
os.chdir(self.script_dir)
return result
else:
print("❌ No tests directory found")
return subprocess.CompletedProcess([], 1)
def show_status(self):
"""Show system status"""
print("📊 Sharey System Status")
print("=" * 30)
print(f"📁 Working directory: {os.getcwd()}")
print(f"🐍 Python version: {sys.version.split()[0]}")
# Virtual environment check
venv_path = Path(".venv")
if venv_path.exists():
print("📦 Virtual environment: ✅ Present")
else:
print("📦 Virtual environment: ❌ Missing")
# Configuration check
config_path = Path("config.json")
if config_path.exists():
print("⚙️ Configuration: ✅ Present")
try:
with open(config_path, 'r') as f:
config_data = json.load(f)
print(f" Host: {config_data.get('host', 'Not set')}")
print(f" Port: {config_data.get('port', 'Not set')}")
print(f" Debug: {config_data.get('debug', 'Not set')}")
except json.JSONDecodeError:
print(" ⚠️ Invalid JSON format")
else:
print("⚙️ Configuration: ❌ Missing")
# Dependencies check
requirements_path = Path("requirements.txt")
if requirements_path.exists():
print("📋 Dependencies file: ✅ Present")
else:
print("📋 Dependencies file: ❌ Missing")
# Project structure
print("\n📂 Project structure:")
structure_dirs = ["src", "tests", "scripts", "docs", "logs"]
for dir_name in structure_dirs:
dir_path = Path(dir_name)
if dir_path.exists():
print(f" {dir_name}/: ✅")
else:
print(f" {dir_name}/: ❌")
def show_logs(self):
"""Show recent logs"""
print("📜 Recent Sharey logs:")
logs_dir = Path("logs")
if logs_dir.exists():
log_files = list(logs_dir.glob("*.log"))
if log_files:
for log_file in sorted(log_files, key=lambda x: x.stat().st_mtime, reverse=True)[:3]:
print(f"\n--- {log_file.name} (last 10 lines) ---")
try:
with open(log_file, 'r') as f:
lines = f.readlines()
for line in lines[-10:]:
print(line.rstrip())
except Exception as e:
print(f"Error reading {log_file}: {e}")
else:
print("No log files found")
else:
print("❌ No logs directory found")
def install_service(self):
"""Install Sharey as a system service (Linux)"""
print("🔧 Installing Sharey as a system service...")
service_content = f"""[Unit]
Description=Sharey File Sharing Platform
After=network.target
[Service]
Type=simple
User={os.getenv('USER')}
WorkingDirectory={os.getcwd()}
Environment=PATH={os.getcwd()}/.venv/bin
ExecStart={os.getcwd()}/.venv/bin/python {os.getcwd()}/run.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
"""
service_file = Path("/tmp/sharey.service")
with open(service_file, 'w') as f:
f.write(service_content)
print("Service file created. To install, run:")
print(f"sudo cp {service_file} /etc/systemd/system/")
print("sudo systemctl daemon-reload")
print("sudo systemctl enable sharey")
print("sudo systemctl start sharey")
def main():
parser = argparse.ArgumentParser(
description="Sharey - Command Line Utility",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Commands:
start Start the application (full checks)
dev Start in development mode (quick)
production Start in production mode with Gunicorn
setup Set up development environment
clean Clean up temporary files
test Run tests
status Show system status
logs Show recent logs
service Install as system service (Linux)
Examples:
python sharey.py start
python sharey.py dev
python sharey.py production
python sharey.py status
"""
)
parser.add_argument('command',
choices=['start', 'dev', 'production', 'setup', 'clean', 'test', 'status', 'logs', 'service'],
help='Command to execute')
if len(sys.argv) == 1:
parser.print_help()
return
args = parser.parse_args()
manager = ShareyManager()
# Execute command
try:
if args.command == 'start':
manager.print_banner()
result = manager.start_app(dev_mode=False)
elif args.command == 'dev':
manager.print_banner()
result = manager.start_app(dev_mode=True)
elif args.command == 'production':
manager.print_banner()
result = manager.start_app(production_mode=True)
elif args.command == 'setup':
manager.print_banner()
manager.setup_environment()
result = subprocess.CompletedProcess([], 0)
elif args.command == 'clean':
manager.print_banner()
manager.clean_project()
result = subprocess.CompletedProcess([], 0)
elif args.command == 'test':
manager.print_banner()
result = manager.run_tests()
elif args.command == 'status':
manager.print_banner()
manager.show_status()
result = subprocess.CompletedProcess([], 0)
elif args.command == 'logs':
manager.print_banner()
manager.show_logs()
result = subprocess.CompletedProcess([], 0)
elif args.command == 'service':
manager.print_banner()
manager.install_service()
result = subprocess.CompletedProcess([], 0)
sys.exit(result.returncode if hasattr(result, 'returncode') else 0)
except KeyboardInterrupt:
print("\n👋 Interrupted by user")
sys.exit(0)
except Exception as e:
print(f"❌ Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

Binary file not shown.

Binary file not shown.

Binary file not shown.

695
src/app.py Normal file
View File

@@ -0,0 +1,695 @@
import os
import random
import string
import uuid
import time
import hashlib
import re
from datetime import datetime, timedelta
from functools import wraps
from urllib.parse import urlparse
from flask import Flask, render_template, request, jsonify, redirect, url_for, send_from_directory, session, Response
from config import config
from storage import StorageManager
import sys
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from expiry_db import ExpiryDatabase
# Flask app configuration
app = Flask(__name__)
# Set Flask configuration from config
flask_config = config.get_flask_config()
app.config['MAX_CONTENT_LENGTH'] = config.get('upload.max_file_size_mb', 100) * 1024 * 1024 # Convert MB to bytes
app.secret_key = config.get('flask.secret_key', os.urandom(24)) # For session management
# Admin helper functions
def hash_password(password):
"""Hash a password using SHA-256"""
return hashlib.sha256(password.encode()).hexdigest()
def verify_password(password, password_hash):
"""Verify a password against its hash"""
return hash_password(password) == password_hash
def require_admin(f):
"""Decorator to require admin authentication"""
@wraps(f)
def decorated_function(*args, **kwargs):
admin_config = config.get('admin', {})
if not admin_config.get('enabled', False):
return jsonify({'error': 'Admin panel is disabled'}), 404
if 'admin_logged_in' not in session:
return redirect(url_for('admin_login'))
# Check session timeout
timeout_minutes = admin_config.get('session_timeout_minutes', 60)
if 'admin_login_time' in session:
login_time = datetime.fromisoformat(session['admin_login_time'])
if datetime.now() - login_time > timedelta(minutes=timeout_minutes):
session.clear()
return redirect(url_for('admin_login'))
return f(*args, **kwargs)
return decorated_function
# Maintenance mode decorator
def check_maintenance(f):
"""Decorator to check if maintenance mode is enabled"""
@wraps(f)
def decorated_function(*args, **kwargs):
maintenance_config = config.get('maintenance', {})
if maintenance_config.get('enabled', False):
# Allow health check even in maintenance mode
if request.endpoint == 'health':
return f(*args, **kwargs)
# Show maintenance page for all other requests
return render_template('maintenance.html',
message=maintenance_config.get('message', 'Sharey is currently under maintenance.'),
estimated_return=maintenance_config.get('estimated_return', '')), 503
return f(*args, **kwargs)
return decorated_function
# Storage Configuration and Initialization
try:
# Validate storage configuration
if not config.validate_storage_config():
storage_config = config.get_storage_config()
backend = storage_config.get('backend', 'unknown')
if backend == 'b2':
print("❌ Invalid B2 configuration. Please check your config.json or environment variables.")
print("Required fields: B2_APPLICATION_KEY_ID, B2_APPLICATION_KEY, B2_BUCKET_NAME")
else:
print(f"❌ Invalid storage configuration for backend: {backend}")
config.print_config()
exit(1)
# Initialize storage manager
storage = StorageManager(config)
backend_info = storage.get_backend_info()
if backend_info['type'] == 'b2':
print(f"✅ Connected to B2 bucket: {backend_info.get('bucket', 'unknown')}")
elif backend_info['type'] == 'local':
print(f"✅ Using local storage at: {backend_info.get('path', 'unknown')}")
# Initialize expiry database
expiry_db = ExpiryDatabase()
except Exception as e:
print(f"❌ Failed to initialize storage: {str(e)}")
print("Please check your storage configuration.")
config.print_config()
exit(1)
# Function to generate a random 6-character string
def generate_short_id(length=6):
return ''.join(random.choices(string.ascii_letters + string.digits, k=length))
# URL Shortener utility functions
def generate_short_code(length=6):
"""Generate a random short code for URL shortener"""
characters = string.ascii_letters + string.digits
while True:
code = ''.join(random.choices(characters, k=length))
# Make sure it doesn't conflict with existing content IDs
content_type, _ = detect_content_type(code)
if content_type is None and not expiry_db.get_redirect(code):
return code
def is_valid_url(url):
"""Validate if a URL is properly formatted"""
try:
result = urlparse(url)
return all([result.scheme, result.netloc]) and result.scheme in ['http', 'https']
except:
return False
def is_safe_url(url):
"""Basic safety check for URLs (can be extended)"""
# Block localhost, private IPs, and suspicious TLDs
parsed = urlparse(url)
hostname = parsed.hostname
if not hostname:
return False
# Block localhost and private IPs
if hostname.lower() in ['localhost', '127.0.0.1', '0.0.0.0']:
return False
# Block private IP ranges (basic check)
if hostname.startswith('192.168.') or hostname.startswith('10.') or hostname.startswith('172.'):
return False
# Block suspicious patterns
suspicious_patterns = [
r'bit\.ly', r'tinyurl', r'short\.link', r'malware', r'phishing'
]
for pattern in suspicious_patterns:
if re.search(pattern, url, re.IGNORECASE):
return False
return True
# Helper function to determine if an ID is a file or paste
def detect_content_type(content_id):
"""
Try to determine if an ID is a file or paste by checking storage
Returns: ('file', file_id) or ('paste', paste_id) or (None, None)
"""
print(f"🔍 Detecting content type for ID: {content_id}")
# First try as file (check if it has an extension or exists in files/)
try:
# Try direct file access
storage.download_file(f"files/{content_id}")
print(f"✅ Found as file: files/{content_id}")
return 'file', content_id
except Exception as e:
print(f"❌ Not found as file: {e}")
# Try as paste
try:
storage.download_file(f"pastes/{content_id}.txt")
print(f"✅ Found as paste: pastes/{content_id}.txt")
return 'paste', content_id
except Exception as e:
print(f"❌ Not found as paste: {e}")
print(f"❌ Content not found: {content_id}")
return None, None
# Main index route
@app.route('/')
@check_maintenance
def index():
return render_template('index.html')
# Clean URL route - handles redirects, files, and pastes at /<id>
@app.route('/<content_id>')
@check_maintenance
def get_content(content_id):
"""
Clean URL handler - checks for URL redirect first, then files/pastes
Examples: sharey.org/ABC123, sharey.org/XYZ789.png
"""
print(f"🔍 Processing request for ID: {content_id}")
# First check if it's a URL redirect
redirect_url = expiry_db.get_redirect(content_id)
if redirect_url:
print(f"🔗 Redirecting {content_id} to {redirect_url}")
return redirect(redirect_url, code=302)
# Not a redirect, try as file/paste
# Skip certain paths that should not be treated as content IDs
excluded_paths = ['admin', 'health', 'api', 'static', 'files', 'pastes', 'favicon.ico']
if content_id in excluded_paths:
return redirect(url_for('index'))
print(f"🔍 Clean URL accessed: {content_id}")
content_type, resolved_id = detect_content_type(content_id)
if content_type == 'file':
print(f"📁 Detected as file: {resolved_id}")
return get_file(resolved_id)
elif content_type == 'paste':
print(f"📝 Detected as paste: {resolved_id}")
return view_paste(resolved_id)
else:
print(f"❌ Content not found: {content_id}")
return render_template('404.html',
title="Content Not Found",
message=f"The content '{content_id}' could not be found."), 404
# Health check endpoint
@app.route('/health')
def health_check():
try:
# Test storage connection by trying to list files
files = storage.list_files()
backend_info = storage.get_backend_info()
return jsonify({
'status': 'healthy',
'storage': f"{backend_info['type']}_connected",
'backend': backend_info
}), 200
except Exception as e:
return jsonify({'status': 'unhealthy', 'error': str(e)}), 500
# Configuration endpoint
@app.route('/api/config')
@check_maintenance
def get_config():
"""Get public configuration (non-sensitive)"""
return jsonify({
'upload': {
'max_file_size_mb': config.get('upload.max_file_size_mb')
},
'paste': {
'max_length': config.get('paste.max_length')
}
})
# File upload route
@app.route('/api/upload', methods=['POST'])
@check_maintenance
def upload_file():
print(f"📤 Upload request received")
files = request.files.getlist('files[]')
expires_at = request.form.get('expires_at') # Get expiry from form data
if not files or not any(file.filename for file in files):
return jsonify({'error': 'No files provided'}), 400
file_urls = []
for file in files:
if file and file.filename:
print(f"📁 Processing file: {file.filename}")
# Generate a 6-character alphanumeric string for the file ID
short_id = generate_short_id()
# Get the file extension
file_extension = os.path.splitext(file.filename)[1]
# Create the file ID with extension
file_id = f"{short_id}{file_extension}"
# Upload file to storage
try:
file_content = file.read()
file_size = len(file_content)
print(f"📊 File size: {file_size} bytes ({file_size/1024/1024:.2f} MB)")
if file_size == 0:
return jsonify({'error': 'Empty file uploaded'}), 400
backend_info = storage.get_backend_info()
print(f"⬆️ Starting {backend_info['type']} upload for {file_id}")
start_time = time.time()
# Prepare metadata
metadata = {
'uploaded_at': datetime.now().isoformat(),
'original_name': file.filename,
'content_type': file.content_type or 'application/octet-stream'
}
# Add expiry metadata if provided
if expires_at:
print(f"⏰ File expiry set to: {expires_at}")
success = storage.upload_file(
file_content=file_content,
file_path=f"files/{file_id}",
content_type=file.content_type or 'application/octet-stream'
)
if not success:
return jsonify({'error': 'Upload failed'}), 500
# Add to expiry database if expiry time is provided
if expires_at:
backend_type = backend_info['type']
expiry_db.add_file(f"files/{file_id}", expires_at, backend_type)
upload_time = time.time() - start_time
print(f"{backend_info['type']} upload successful for {file_id} in {upload_time:.2f} seconds")
# Use clean URL format: sharey.org/ABC123.png (new format)
download_url = url_for('get_content', content_id=file_id, _external=True)
file_urls.append(download_url)
print(f"🔗 Generated clean URL: {download_url}")
except Exception as e:
print(f"❌ Upload failed for {file.filename}: {str(e)}")
return jsonify({'error': f'Failed to upload file: {str(e)}'}), 500
print(f"🎉 All uploads completed successfully")
return jsonify({'urls': file_urls}), 201
# Legacy file route (for backward compatibility)
# Old format: /files/ABC123.png
@app.route('/files/<file_id>', methods=['GET'])
@check_maintenance
def get_file(file_id):
try:
# Download file from storage and serve it through Flask
file_content = storage.download_file(f"files/{file_id}")
# Get the content type from the original upload or guess it
content_type = 'application/octet-stream'
file_extension = os.path.splitext(file_id)[1].lower()
# Simple content type mapping
content_types = {
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
'.gif': 'image/gif', '.pdf': 'application/pdf', '.txt': 'text/plain',
'.mp4': 'video/mp4', '.mp3': 'audio/mpeg', '.zip': 'application/zip'
}
content_type = content_types.get(file_extension, 'application/octet-stream')
# Return the file content with proper headers
return Response(
file_content,
mimetype=content_type,
headers={
'Content-Disposition': f'inline; filename="{file_id}"',
'Cache-Control': 'public, max-age=3600'
}
)
except Exception as e:
return jsonify({'error': f'File not found: {str(e)}'}), 404
# Paste creation route
@app.route('/api/paste', methods=['POST'])
@check_maintenance
def create_paste():
data = request.get_json()
content = data.get('content')
expires_at = data.get('expires_at') # Get expiry from request data
if not content:
return jsonify({'error': 'Content cannot be empty'}), 400
# Check paste length limit
max_length = config.get('paste.max_length', 1000000)
if len(content) > max_length:
return jsonify({
'error': f'Paste too long. Maximum length: {max_length} characters'
}), 400
unique_id = str(uuid.uuid4())
paste_id = unique_id[:6] # Shorten the UUID for pastes
try:
# Prepare metadata
metadata = {
'uploaded_at': datetime.now().isoformat(),
'content_type': 'text/plain; charset=utf-8',
'content_length': len(content)
}
# Add expiry metadata if provided
if expires_at:
metadata['expires_at'] = expires_at
print(f"⏰ Paste expiry set to: {expires_at}")
# Upload paste to storage
success = storage.upload_file(
file_content=content.encode('utf-8'),
file_path=f"pastes/{paste_id}.txt",
content_type='text/plain; charset=utf-8',
metadata=metadata
)
if not success:
return jsonify({'error': 'Failed to create paste'}), 500
# Use clean URL format: sharey.org/ABC123 (new format)
paste_url = url_for('get_content', content_id=paste_id, _external=True)
return jsonify({'url': paste_url}), 201
except Exception as e:
return jsonify({'error': f'Failed to create paste: {str(e)}'}), 500
# Legacy paste routes (for backward compatibility)
# Old format: /pastes/ABC123
@app.route('/pastes/<paste_id>', methods=['GET'])
@check_maintenance
def view_paste(paste_id):
try:
# Download paste content from storage
content_bytes = storage.download_file(f"pastes/{paste_id}.txt")
content = content_bytes.decode('utf-8')
return render_template('view_paste.html', content=content, paste_id=paste_id)
except Exception as e:
return jsonify({'error': 'Paste not found'}), 404
@app.route('/pastes/raw/<paste_id>', methods=['GET'])
@check_maintenance
def view_paste_raw(paste_id):
try:
# Download paste content from storage
content_bytes = storage.download_file(f"pastes/{paste_id}.txt")
content = content_bytes.decode('utf-8')
return content, 200, {'Content-Type': 'text/plain; charset=utf-8'}
except Exception as e:
return jsonify({'error': 'Paste not found'}), 404
# URL Shortener API Routes
@app.route('/api/shorten', methods=['POST'])
@check_maintenance
def create_redirect():
"""Create a new URL redirect"""
try:
data = request.get_json()
if not data or 'url' not in data:
return jsonify({'error': 'URL is required'}), 400
target_url = data['url'].strip()
custom_code = data.get('code', '').strip()
expires_in_hours = data.get('expires_in_hours')
# Validate URL
if not is_valid_url(target_url):
return jsonify({'error': 'Invalid URL format'}), 400
if not is_safe_url(target_url):
return jsonify({'error': 'URL not allowed'}), 400
# Generate or validate short code
if custom_code:
# Custom code provided
if len(custom_code) < 3 or len(custom_code) > 20:
return jsonify({'error': 'Custom code must be 3-20 characters'}), 400
if not re.match(r'^[a-zA-Z0-9_-]+$', custom_code):
return jsonify({'error': 'Custom code can only contain letters, numbers, hyphens, and underscores'}), 400
# Check if code already exists
if expiry_db.get_redirect(custom_code) or detect_content_type(custom_code)[0]:
return jsonify({'error': 'Code already exists'}), 400
short_code = custom_code
else:
# Generate random code
short_code = generate_short_code()
# Calculate expiry
expires_at = None
if expires_in_hours and expires_in_hours > 0:
expires_at = (datetime.utcnow() + timedelta(hours=expires_in_hours)).isoformat() + 'Z'
# Get client IP for logging
client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
# Create redirect
success = expiry_db.add_redirect(
short_code=short_code,
target_url=target_url,
expires_at=expires_at,
created_by_ip=client_ip
)
if not success:
return jsonify({'error': 'Failed to create redirect'}), 500
# Return short URL
short_url = url_for('get_content', content_id=short_code, _external=True)
return jsonify({
'short_url': short_url,
'short_code': short_code,
'target_url': target_url,
'expires_at': expires_at
}), 201
except Exception as e:
return jsonify({'error': f'Failed to create redirect: {str(e)}'}), 500
@app.route('/api/redirect/<short_code>/info', methods=['GET'])
@check_maintenance
def redirect_info(short_code):
"""Get information about a redirect (for preview)"""
try:
import sqlite3
# Get redirect info without incrementing click count
conn = sqlite3.connect(expiry_db.db_path)
cursor = conn.cursor()
cursor.execute('''
SELECT target_url, created_at, expires_at, click_count, is_active
FROM url_redirects
WHERE short_code = ?
''', (short_code,))
result = cursor.fetchone()
conn.close()
if not result:
return jsonify({'error': 'Redirect not found'}), 404
target_url, created_at, expires_at, click_count, is_active = result
# Check if expired
is_expired = False
if expires_at:
current_time = datetime.utcnow().isoformat() + 'Z'
is_expired = expires_at <= current_time
return jsonify({
'short_code': short_code,
'target_url': target_url,
'created_at': created_at,
'expires_at': expires_at,
'click_count': click_count,
'is_active': bool(is_active),
'is_expired': is_expired
})
except Exception as e:
return jsonify({'error': f'Failed to get redirect info: {str(e)}'}), 500
# Admin Panel Routes
@app.route('/admin')
@require_admin
def admin_panel():
"""Admin panel dashboard"""
admin_config = config.get('admin', {})
maintenance_config = config.get('maintenance', {})
return render_template('admin.html',
maintenance_enabled=maintenance_config.get('enabled', False),
maintenance_message=maintenance_config.get('message', ''),
estimated_return=maintenance_config.get('estimated_return', ''),
config=config.config
)
@app.route('/admin/login', methods=['GET', 'POST'])
def admin_login():
"""Admin login page"""
admin_config = config.get('admin', {})
if not admin_config.get('enabled', False):
return jsonify({'error': 'Admin panel is disabled'}), 404
if not admin_config.get('password_hash'):
return render_template('admin_login.html',
error='Admin password not configured. Please set admin.password_hash in config.json')
if request.method == 'POST':
password = request.form.get('password')
if password and verify_password(password, admin_config['password_hash']):
session['admin_logged_in'] = True
session['admin_login_time'] = datetime.now().isoformat()
return redirect(url_for('admin_panel'))
else:
return render_template('admin_login.html', error='Invalid password')
return render_template('admin_login.html')
@app.route('/admin/logout')
def admin_logout():
"""Admin logout"""
session.clear()
return redirect(url_for('admin_login'))
@app.route('/admin/maintenance', methods=['POST'])
@require_admin
def admin_maintenance():
"""Toggle maintenance mode"""
action = request.form.get('action')
message = request.form.get('maintenance_message', '')
estimated_return = request.form.get('estimated_return', '')
current_config = config.config.copy()
if action == 'enable':
current_config['maintenance']['enabled'] = True
current_config['maintenance']['message'] = message
current_config['maintenance']['estimated_return'] = estimated_return
elif action == 'disable':
current_config['maintenance']['enabled'] = False
# Save the updated configuration
if config.save_config(current_config):
# Reload config to reflect changes
config.config = config._load_config()
return redirect(url_for('admin_panel'))
@app.route('/admin/config', methods=['POST'])
@require_admin
def admin_config():
"""Update configuration"""
max_file_size = request.form.get('max_file_size')
max_paste_length = request.form.get('max_paste_length')
current_config = config.config.copy()
if max_file_size:
try:
current_config['upload']['max_file_size_mb'] = int(max_file_size)
except ValueError:
pass
if max_paste_length:
try:
current_config['paste']['max_length'] = int(max_paste_length)
except ValueError:
pass
# Save the updated configuration
if config.save_config(current_config):
# Reload config to reflect changes
config.config = config._load_config()
# Update Flask config
app.config['MAX_CONTENT_LENGTH'] = config.get('upload.max_file_size_mb', 100) * 1024 * 1024
return redirect(url_for('admin_panel'))
@app.route('/admin/urls')
@require_admin
def admin_urls():
"""Admin page for managing URL redirects"""
redirects = expiry_db.list_redirects(limit=100)
return render_template('admin.html',
tab='urls',
redirects=redirects)
@app.route('/admin/urls/disable/<short_code>', methods=['POST'])
@require_admin
def admin_disable_redirect(short_code):
"""Disable a URL redirect"""
success = expiry_db.disable_redirect(short_code)
if success:
return jsonify({'success': True, 'message': f'Redirect {short_code} disabled'})
else:
return jsonify({'success': False, 'message': 'Failed to disable redirect'}), 404
# Error handling for 404 routes
@app.errorhandler(404)
def page_not_found(e):
return jsonify({'error': 'Page not found'}), 404
# Run the Flask app
if __name__ == '__main__':
flask_config = config.get_flask_config()
app.run(
host=flask_config['host'],
port=flask_config['port'],
debug=flask_config['debug']
)

187
src/config.py Normal file
View File

@@ -0,0 +1,187 @@
"""
Configuration management for Sharey
Supports both config.json and environment variables
"""
import os
import json
from typing import Dict, Any, Optional
class Config:
"""Configuration handler for Sharey"""
def __init__(self, config_file: str = "config.json"):
self.config_file = config_file
self.config = self._load_config()
def _load_config(self) -> Dict[str, Any]:
"""Load configuration from JSON file with environment variable fallbacks"""
config = {}
# Try to load from JSON file first
if os.path.exists(self.config_file):
try:
with open(self.config_file, 'r') as f:
config = json.load(f)
print(f"✅ Loaded configuration from {self.config_file}")
except (json.JSONDecodeError, FileNotFoundError) as e:
print(f"⚠️ Error loading {self.config_file}: {e}")
print("Falling back to environment variables...")
else:
print(f"⚠️ {self.config_file} not found, using environment variables")
# Set defaults and apply environment variable overrides
config = self._apply_defaults_and_env(config)
return config
def _apply_defaults_and_env(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""Apply default values and environment variable overrides"""
# Storage Configuration
storage_config = config.get('storage', {})
storage_config['backend'] = storage_config.get('backend', os.getenv('STORAGE_BACKEND', 'b2'))
storage_config['local_path'] = storage_config.get('local_path', os.getenv('STORAGE_LOCAL_PATH', 'storage'))
config['storage'] = storage_config
# B2 Configuration
b2_config = config.get('b2', {})
b2_config['application_key_id'] = (
b2_config.get('application_key_id') or
os.getenv('B2_APPLICATION_KEY_ID', 'your_key_id_here')
)
b2_config['application_key'] = (
b2_config.get('application_key') or
os.getenv('B2_APPLICATION_KEY', 'your_application_key_here')
)
b2_config['bucket_name'] = (
b2_config.get('bucket_name') or
os.getenv('B2_BUCKET_NAME', 'your_bucket_name_here')
)
config['b2'] = b2_config
# Flask Configuration
flask_config = config.get('flask', {})
flask_config['host'] = flask_config.get('host', os.getenv('FLASK_HOST', '127.0.0.1'))
flask_config['port'] = int(flask_config.get('port', os.getenv('FLASK_PORT', 8866)))
flask_config['debug'] = flask_config.get('debug', os.getenv('FLASK_DEBUG', 'true').lower() == 'true')
config['flask'] = flask_config
# Upload Configuration
upload_config = config.get('upload', {})
upload_config['max_file_size_mb'] = upload_config.get('max_file_size_mb', 100)
config['upload'] = upload_config
# Paste Configuration
paste_config = config.get('paste', {})
paste_config['max_length'] = paste_config.get('max_length', 1000000)
config['paste'] = paste_config
# Security Configuration
security_config = config.get('security', {})
security_config['rate_limit_enabled'] = security_config.get('rate_limit_enabled', False)
security_config['max_uploads_per_hour'] = security_config.get('max_uploads_per_hour', 50)
config['security'] = security_config
return config
def get(self, key: str, default: Any = None) -> Any:
"""Get configuration value using dot notation (e.g., 'b2.bucket_name')"""
keys = key.split('.')
value = self.config
for k in keys:
if isinstance(value, dict) and k in value:
value = value[k]
else:
return default
return value
def validate_b2_config(self) -> bool:
"""Validate B2 configuration"""
required_fields = ['application_key_id', 'application_key', 'bucket_name']
invalid_values = ['your_key_id_here', 'your_application_key_here', 'your_bucket_name_here', '']
for field in required_fields:
value = self.get(f'b2.{field}')
if not value or value in invalid_values:
return False
return True
def validate_storage_config(self) -> bool:
"""Validate storage configuration"""
backend = self.get('storage.backend', 'b2').lower()
if backend == 'b2':
return self.validate_b2_config()
elif backend == 'local':
# Local storage is always valid (will create directory if needed)
return True
else:
print(f"❌ Unknown storage backend: {backend}")
return False
def get_storage_config(self) -> Dict[str, Any]:
"""Get storage configuration as dictionary"""
return {
'backend': self.get('storage.backend', 'b2'),
'local_path': self.get('storage.local_path', 'storage')
}
def get_b2_config(self) -> Dict[str, str]:
"""Get B2 configuration as dictionary"""
return {
'key_id': self.get('b2.application_key_id'),
'key': self.get('b2.application_key'),
'bucket_name': self.get('b2.bucket_name')
}
def get_flask_config(self) -> Dict[str, Any]:
"""Get Flask configuration as dictionary"""
return {
'host': self.get('flask.host'),
'port': self.get('flask.port'),
'debug': self.get('flask.debug')
}
def save_config(self, config_dict: Dict[str, Any], backup: bool = True) -> bool:
"""Save configuration to JSON file"""
try:
# Create backup if requested
if backup and os.path.exists(self.config_file):
backup_file = f"{self.config_file}.backup"
os.rename(self.config_file, backup_file)
print(f"💾 Created backup: {backup_file}")
# Save new configuration
with open(self.config_file, 'w') as f:
json.dump(config_dict, f, indent=2)
print(f"✅ Configuration saved to {self.config_file}")
return True
except Exception as e:
print(f"❌ Error saving configuration: {e}")
return False
def print_config(self, hide_secrets: bool = True) -> None:
"""Print current configuration"""
print("\n📋 Current Configuration:")
print("=" * 50)
config_copy = json.loads(json.dumps(self.config)) # Deep copy
if hide_secrets:
# Hide sensitive information
if 'b2' in config_copy:
if 'application_key_id' in config_copy['b2']:
config_copy['b2']['application_key_id'] = '***HIDDEN***'
if 'application_key' in config_copy['b2']:
config_copy['b2']['application_key'] = '***HIDDEN***'
print(json.dumps(config_copy, indent=2))
print("=" * 50)
# Global configuration instance
config = Config()

347
src/config_util.py Normal file
View File

@@ -0,0 +1,347 @@
#!/usr/bin/env python3
"""
Configuration Management Utility for Sharey
"""
import sys
import json
import hashlib
import getpass
from config import config
def show_config():
"""Display current configuration"""
config.print_config(hide_secrets=False)
def validate_config():
"""Validate current configuration"""
print("🔧 Validating configuration...")
# Check storage configuration
if config.validate_storage_config():
storage_config = config.get_storage_config()
backend = storage_config.get('backend', 'unknown')
print(f"✅ Storage configuration is valid (backend: {backend})")
if backend == 'b2':
b2_config = config.get_b2_config()
print(f" → B2 bucket: {b2_config.get('bucket_name', 'unknown')}")
elif backend == 'local':
local_path = storage_config.get('local_path', 'storage')
print(f" → Local path: {local_path}")
else:
print("❌ Storage configuration is invalid")
return False
# Check other configurations
max_size = config.get('upload.max_file_size_mb')
if max_size and max_size > 0:
print(f"✅ Upload max size: {max_size} MB")
else:
print("⚠️ Upload max size not configured")
max_length = config.get('paste.max_length')
if max_length and max_length > 0:
print(f"✅ Paste max length: {max_length:,} characters")
else:
print("⚠️ Paste max length not configured")
print("✅ Configuration validation complete")
return True
def set_config():
"""Interactively set configuration values"""
print("🔧 Interactive Configuration Setup")
print("=" * 50)
current_config = config.config.copy()
# Storage Configuration
print("\n💾 Storage Configuration:")
storage_backend = input(f"Storage backend (b2/local) [{current_config.get('storage', {}).get('backend', 'b2')}]: ").strip().lower()
if storage_backend in ['b2', 'local']:
if 'storage' not in current_config:
current_config['storage'] = {}
current_config['storage']['backend'] = storage_backend
if storage_backend == 'local':
local_path = input(f"Local storage path [{current_config.get('storage', {}).get('local_path', 'storage')}]: ").strip()
if local_path:
current_config['storage']['local_path'] = local_path
# B2 Configuration (only if using B2 backend)
if current_config.get('storage', {}).get('backend', 'b2') == 'b2':
print("\n📦 Backblaze B2 Configuration:")
current_config['b2']['application_key_id'] = input(f"Application Key ID [{current_config['b2'].get('application_key_id', '')}]: ").strip() or current_config['b2'].get('application_key_id', '')
current_config['b2']['application_key'] = input(f"Application Key [{current_config['b2'].get('application_key', '')}]: ").strip() or current_config['b2'].get('application_key', '')
current_config['b2']['bucket_name'] = input(f"Bucket Name [{current_config['b2'].get('bucket_name', '')}]: ").strip() or current_config['b2'].get('bucket_name', '')
# Flask Configuration
print("\n🌐 Flask Configuration:")
current_config['flask']['host'] = input(f"Host [{current_config['flask'].get('host', '127.0.0.1')}]: ").strip() or current_config['flask'].get('host', '127.0.0.1')
port_input = input(f"Port [{current_config['flask'].get('port', 8866)}]: ").strip()
if port_input:
try:
current_config['flask']['port'] = int(port_input)
except ValueError:
print("⚠️ Invalid port, keeping current value")
debug_input = input(f"Debug mode (true/false) [{current_config['flask'].get('debug', True)}]: ").strip().lower()
if debug_input in ['true', 'false']:
current_config['flask']['debug'] = debug_input == 'true'
# Upload Configuration
print("\n📁 Upload Configuration:")
size_input = input(f"Max file size (MB) [{current_config['upload'].get('max_file_size_mb', 100)}]: ").strip()
if size_input:
try:
current_config['upload']['max_file_size_mb'] = int(size_input)
except ValueError:
print("⚠️ Invalid size, keeping current value")
# Paste Configuration
print("\n📝 Paste Configuration:")
length_input = input(f"Max paste length [{current_config['paste'].get('max_length', 1000000)}]: ").strip()
if length_input:
try:
current_config['paste']['max_length'] = int(length_input)
except ValueError:
print("⚠️ Invalid length, keeping current value")
# Save configuration
if config.save_config(current_config):
print("\n✅ Configuration saved successfully!")
return True
else:
print("\n❌ Failed to save configuration")
return False
def reset_config():
"""Reset configuration to defaults"""
print("🔄 Resetting configuration to defaults...")
default_config = {
"b2": {
"application_key_id": "your_key_id_here",
"application_key": "your_application_key_here",
"bucket_name": "your_bucket_name_here"
},
"flask": {
"host": "127.0.0.1",
"port": 8866,
"debug": True
},
"upload": {
"max_file_size_mb": 100
},
"paste": {
"max_length": 1000000
},
"security": {
"rate_limit_enabled": False,
"max_uploads_per_hour": 50
}
}
if config.save_config(default_config):
print("✅ Configuration reset to defaults")
return True
else:
print("❌ Failed to reset configuration")
return False
def enable_maintenance():
"""Enable maintenance mode"""
print("🔧 Enabling maintenance mode...")
current_config = config.config.copy()
# Ensure maintenance section exists
if 'maintenance' not in current_config:
current_config['maintenance'] = {}
current_config['maintenance']['enabled'] = True
# Ask for custom message
message = input("Maintenance message [Sharey is currently under maintenance. Please check back later!]: ").strip()
if message:
current_config['maintenance']['message'] = message
# Ask for estimated return time
return_time = input("Estimated return time (optional): ").strip()
if return_time:
current_config['maintenance']['estimated_return'] = return_time
# Save configuration
if config.save_config(current_config):
print("✅ Maintenance mode enabled!")
print("💡 Users will now see the maintenance page")
print("💡 To disable: python config_util.py maintenance disable")
else:
print("❌ Failed to enable maintenance mode")
def disable_maintenance():
"""Disable maintenance mode"""
print("🔧 Disabling maintenance mode...")
current_config = config.config.copy()
# Ensure maintenance section exists
if 'maintenance' not in current_config:
current_config['maintenance'] = {}
current_config['maintenance']['enabled'] = False
# Save configuration
if config.save_config(current_config):
print("✅ Maintenance mode disabled!")
print("💡 Sharey is now accessible to users")
else:
print("❌ Failed to disable maintenance mode")
def maintenance_status():
"""Show maintenance mode status"""
maintenance_config = config.get('maintenance', {})
enabled = maintenance_config.get('enabled', False)
print("🔧 Maintenance Mode Status")
print("=" * 30)
if enabled:
print("🟠 Status: ENABLED")
print(f"📝 Message: {maintenance_config.get('message', 'Default message')}")
if maintenance_config.get('estimated_return'):
print(f"⏰ Estimated return: {maintenance_config['estimated_return']}")
print("\n💡 To disable: python config_util.py maintenance disable")
else:
print("🟢 Status: DISABLED")
print("💡 Sharey is accessible to users")
print("💡 To enable: python config_util.py maintenance enable")
def handle_maintenance_command():
"""Handle maintenance mode subcommands"""
if len(sys.argv) < 3:
print("Usage: python config_util.py maintenance <enable|disable|status>")
print("Commands:")
print(" enable - Enable maintenance mode")
print(" disable - Disable maintenance mode")
print(" status - Show current maintenance status")
return
subcommand = sys.argv[2].lower()
if subcommand == 'enable':
enable_maintenance()
elif subcommand == 'disable':
disable_maintenance()
elif subcommand == 'status':
maintenance_status()
else:
print(f"Unknown maintenance command: {subcommand}")
def set_admin_password():
"""Set admin panel password"""
print("🔐 Setting Admin Panel Password")
print("=" * 35)
# Get password with confirmation
while True:
password = getpass.getpass("Enter admin password: ")
if len(password) < 6:
print("❌ Password must be at least 6 characters long")
continue
confirm = getpass.getpass("Confirm admin password: ")
if password != confirm:
print("❌ Passwords don't match")
continue
break
# Hash the password
password_hash = hashlib.sha256(password.encode()).hexdigest()
# Update config
current_config = config.config.copy()
# Ensure admin section exists
if 'admin' not in current_config:
current_config['admin'] = {}
current_config['admin']['password_hash'] = password_hash
current_config['admin']['session_timeout_minutes'] = current_config['admin'].get('session_timeout_minutes', 30)
# Save configuration
if config.save_config(current_config):
print("✅ Admin password set successfully!")
print("💡 You can now access the admin panel at /admin")
else:
print("❌ Failed to save admin password")
def admin_status():
"""Show admin panel configuration status"""
admin_config = config.get('admin', {})
print("🔐 Admin Panel Status")
print("=" * 25)
if admin_config.get('password_hash'):
print("🟢 Status: CONFIGURED")
print(f"⏰ Session timeout: {admin_config.get('session_timeout_minutes', 30)} minutes")
print("💡 Access at: /admin")
print("💡 To change password: python config_util.py admin password")
else:
print("🟠 Status: NOT CONFIGURED")
print("💡 To set password: python config_util.py admin password")
def handle_admin_command():
"""Handle admin panel subcommands"""
if len(sys.argv) < 3:
print("Usage: python config_util.py admin <password|status>")
print("Commands:")
print(" password - Set admin panel password")
print(" status - Show admin panel configuration status")
return
subcommand = sys.argv[2].lower()
if subcommand == 'password':
set_admin_password()
elif subcommand == 'status':
admin_status()
else:
print(f"Unknown admin command: {subcommand}")
def main():
"""Main configuration utility"""
if len(sys.argv) < 2:
print("Sharey Configuration Utility")
print("Usage: python config_util.py <command>")
print("\nCommands:")
print(" show - Display current configuration")
print(" validate - Validate configuration")
print(" set - Interactively set configuration")
print(" reset - Reset configuration to defaults")
print(" maintenance - Manage maintenance mode (enable/disable/status)")
print(" admin - Manage admin panel (password/status)")
sys.exit(1)
command = sys.argv[1].lower()
if command == 'show':
show_config()
elif command == 'validate':
validate_config()
elif command == 'set':
set_config()
elif command == 'reset':
reset_config()
elif command == 'maintenance':
handle_maintenance_command()
elif command == 'admin':
handle_admin_command()
else:
print(f"Unknown command: {command}")
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,39 @@
// Image preview functionality for Sharey
// Helper function to check if file is an image
function isImageFile(file) {
const imageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', 'image/bmp'];
return imageTypes.includes(file.type.toLowerCase());
}
// Helper function to create image preview
function createImagePreview(fileUrl, fileName) {
const preview = document.createElement('div');
preview.className = 'image-preview';
preview.style.cssText = `
margin: 15px 0;
text-align: center;
border: 2px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
background: #f9f9f9;
`;
const img = document.createElement('img');
img.src = fileUrl;
img.alt = fileName;
img.style.cssText = `
max-width: 100%;
max-height: 300px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
object-fit: contain;
`;
// Handle image load errors
img.onerror = () => {
preview.innerHTML = '<p style="color: #666; font-style: italic;">📷 Image preview not available</p>';
};
preview.appendChild(img);
return preview;
}

1305
src/static/script.js Normal file

File diff suppressed because it is too large Load Diff

619
src/static/script_backup.js Normal file
View File

@@ -0,0 +1,619 @@
document.addEventListener("DOMContentLoaded", function() {
console.log('🔥 Script.js loaded!');
// Theme management
initializeTheme();
const dropArea = document.getElementById('drop-area');
const fileInput = document.getElementById('fileInput');
const progressContainer = document.getElementById('progressContainer');
const function updateThemeButton(themeSetting) {
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
if (themeSetting === 'light') {
themeToggle.textContent = '🌙';
themeToggle.title = 'Switch to dark mode';
} else {
themeToggle.textContent = '☀️';
themeToggle.title = 'Switch to light mode';
}
}
} = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const result = document.getElementById('result');
const fileModeButton = document.getElementById('fileMode');
const pasteModeButton = document.getElementById('pasteMode');
const faqButton = document.getElementById('faqButton');
const fileSharingSection = document.getElementById('fileSharingSection');
const pastebinSection = document.getElementById('pastebinSection');
const faqSection = document.getElementById('faqSection');
const pasteContent = document.getElementById('pasteContent');
const submitPasteButton = document.getElementById('submitPaste');
const pasteResult = document.getElementById('pasteResult');
let filesToUpload = [];
// Toggle between file sharing, pastebin, and FAQ sections
fileModeButton.addEventListener('click', () => {
showSection(fileSharingSection);
hideSection(pastebinSection);
hideSection(faqSection);
activateButton(fileModeButton);
});
pasteModeButton.addEventListener('click', () => {
showSection(pastebinSection);
hideSection(fileSharingSection);
hideSection(faqSection);
activateButton(pasteModeButton);
});
faqButton.addEventListener('click', () => {
showSection(faqSection);
hideSection(fileSharingSection);
hideSection(pastebinSection);
activateButton(faqButton);
});
// Helper function to show a section
function showSection(section) {
section.style.display = 'block';
}
// Helper function to hide a section
function hideSection(section) {
section.style.display = 'none';
}
// Helper function to activate a button
function activateButton(button) {
const buttons = [fileModeButton, pasteModeButton, faqButton];
buttons.forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
}
// Make drop area clickable
dropArea.addEventListener('click', () => {
fileInput.click();
});
// Drag-and-drop events
dropArea.addEventListener('dragover', (event) => {
event.preventDefault();
dropArea.classList.add('dragging');
});
dropArea.addEventListener('dragleave', () => {
dropArea.classList.remove('dragging');
});
dropArea.addEventListener('drop', (event) => {
event.preventDefault();
dropArea.classList.remove('dragging');
filesToUpload = event.dataTransfer.files;
displaySelectedFiles(filesToUpload);
handleFileUpload(filesToUpload);
});
// Handle file input selection
fileInput.addEventListener('change', (event) => {
filesToUpload = event.target.files;
displaySelectedFiles(filesToUpload);
handleFileUpload(filesToUpload);
});
// Helper function to display selected files
function displaySelectedFiles(files) {
result.innerHTML = '<p><3E> Uploading files...</p>';
}
// Handle file upload (simplified, no encryption)
async function handleFileUpload(files) {
console.log('<27> Starting file upload...');
// Check file sizes first (default 100MB)
const maxSizeMB = 100;
for (let file of files) {
if (file.size > maxSizeMB * 1024 * 1024) {
result.innerHTML = `<div class="error">File too large: ${file.name} (${(file.size/1024/1024).toFixed(2)}MB). Max allowed: ${maxSizeMB}MB.</div>`;
return;
}
}
try {
progressContainer.style.display = 'block';
progressText.textContent = 'Uploading files...';
const uploadPromises = Array.from(files).map(async (file, index) => {
try {
console.log(`<EFBFBD> Uploading file: ${file.name}`);
// Create form data with file
const formData = new FormData();
formData.append('file', file);
// Upload file
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
const result = await response.json();
console.log('📤 Upload response:', result);
return {
url: result.urls[0],
file: file,
originalSize: file.size
};
} catch (error) {
console.error(`❌ Failed to upload ${file.name}:`, error);
throw error;
}
});
// Wait for all uploads to complete
const results = await Promise.all(uploadPromises);
// Update progress
progressBar.style.width = '100%';
progressText.textContent = '100%';
// Display results
displayUploadedFiles(results);
} catch (error) {
console.error('❌ Upload process failed:', error);
result.innerHTML = `<div class="error">Upload failed: ${error.message}</div>`;
} finally {
// Hide progress after a short delay
setTimeout(() => {
progressContainer.style.display = 'none';
progressBar.style.width = '0%';
progressText.textContent = '0%';
}, 2000);
}
}
// Display uploaded files
function displayUploadedFiles(results) {
console.log('<27> Displaying file results:', results);
result.innerHTML = '';
const header = document.createElement('div');
header.innerHTML = '<h3><3E> Files Uploaded</h3>';
result.appendChild(header);
results.forEach((fileResult, index) => {
const fileContainer = document.createElement('div');
fileContainer.style.marginBottom = '15px';
fileContainer.style.padding = '15px';
fileContainer.style.border = '2px solid #4CAF50';
fileContainer.style.borderRadius = '8px';
fileContainer.style.backgroundColor = '#f0f8f0';
const fileName = document.createElement('div');
fileName.innerHTML = `<strong>📄 ${fileResult.file.name}</strong>`;
fileName.style.marginBottom = '10px';
fileContainer.appendChild(fileName);
const fileSize = document.createElement('div');
fileSize.innerHTML = `<EFBFBD> Size: ${(fileResult.originalSize / 1024).toFixed(1)} KB`;
fileSize.style.marginBottom = '10px';
fileSize.style.fontSize = '14px';
fileContainer.appendChild(fileSize);
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.gap = '10px';
buttonContainer.style.flexWrap = 'wrap';
// Share button
const shareButton = document.createElement('button');
shareButton.textContent = '🔗 Copy Link';
shareButton.className = 'share-button';
shareButton.addEventListener('click', () => {
navigator.clipboard.writeText(fileResult.url).then(() => {
shareButton.textContent = '✅ Copied!';
setTimeout(() => {
shareButton.textContent = '🔗 Copy Link';
}, 2000);
});
});
buttonContainer.appendChild(shareButton);
// View button
const viewButton = document.createElement('button');
viewButton.textContent = '👁️ View';
viewButton.className = 'view-button';
viewButton.addEventListener('click', () => {
window.open(fileResult.url, '_blank');
});
buttonContainer.appendChild(viewButton);
// QR Code button
const qrButton = document.createElement('button');
qrButton.textContent = '📱 QR Code';
qrButton.className = 'qr-button';
qrButton.style.backgroundColor = '#9C27B0';
qrButton.style.color = 'white';
qrButton.addEventListener('click', () => {
console.log('🔍 QR button clicked for file:', fileResult.file.name);
showQRCode(fileResult.url, fileResult.file.name);
});
buttonContainer.appendChild(qrButton);
fileContainer.appendChild(buttonContainer);
result.appendChild(fileContainer);
});
}
// Handle paste submission (simplified, no encryption)
submitPasteButton.addEventListener('click', async () => {
const content = pasteContent.value.trim();
if (!content) {
pasteResult.innerHTML = '<p class="error">Please enter some content</p>';
return;
}
try {
pasteResult.innerHTML = '<p><3E> Uploading paste...</p>';
// Upload paste
const response = await fetch('/api/paste', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
content: content
})
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.statusText}`);
}
const result = await response.json();
const url = result.url;
// Display result
displayPasteResult(url, content.length);
// Clear the textarea
pasteContent.value = '';
} catch (error) {
console.error('❌ Paste upload failed:', error);
pasteResult.innerHTML = `<div class="error">Paste upload failed: ${error.message}</div>`;
}
});
// Display paste result
function displayPasteResult(url, originalLength) {
pasteResult.innerHTML = `
<div style="border: 2px solid #4CAF50; border-radius: 8px; padding: 15px; background: #f0f8f0; margin-top: 15px;">
<h3><3E> Paste Uploaded</h3>
<p>📝 Content: ${originalLength} characters</p>
<div style="display: flex; gap: 10px; margin-top: 10px;">
<button class="share-button" onclick="navigator.clipboard.writeText('${url}').then(() => { this.textContent = '✅ Copied!'; setTimeout(() => this.textContent = '🔗 Copy Link', 2000); })">🔗 Copy Link</button>
<button class="view-button" onclick="window.open('${url}', '_blank')">👁️ View</button>
<button class="qr-button" style="background-color: #9C27B0; color: white;" onclick="showQRCode('${url}', 'Paste (${originalLength} chars)')">📱 QR Code</button>
</div>
</div>
`;
}
// Theme management functions
function initializeTheme() {
// Get saved theme from cookie or default to light
const savedTheme = getCookie('sharey-theme') || 'light';
// Apply the theme
applyTheme(savedTheme);
// Set up theme toggle button
const themeToggle = document.getElementById('themeToggle');
console.log('🔍 Theme toggle button found:', themeToggle ? 'YES' : 'NO');
if (themeToggle) {
console.log('🎯 Adding click listener to theme button');
themeToggle.addEventListener('click', toggleTheme);
// Show the current theme setting on button
updateThemeButton(savedTheme);
} else {
console.log('⚠️ Theme toggle button not found on this page');
}
}
function toggleTheme() {
console.log('🎨 Theme toggle button clicked!');
const currentTheme = getCookie('sharey-theme') || 'light';
console.log('🎨 Current theme:', currentTheme);
let newTheme;
// Toggle between light and dark only
if (currentTheme === 'light') {
newTheme = 'dark';
} else {
newTheme = 'light';
}
console.log('🎨 Switching to theme:', newTheme);
applyTheme(newTheme);
setCookie('sharey-theme', newTheme, 365); // Save for 1 year
updateThemeButton(newTheme);
console.log(`🎨 Theme switched to: ${newTheme}`);
}
function applyTheme(themeSetting) {
if (themeSetting === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.removeAttribute('data-theme');
}
}
function getSystemTheme() {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
function updateThemeButton(themeSetting) {
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
if (themeSetting === 'light') {
themeToggle.textContent = '<27>';
themeToggle.title = 'Switch to dark mode';
} else {
themeToggle.textContent = '☀️';
themeToggle.title = 'Switch to light mode';
}
}
}
// Cookie management functions
function setCookie(name, value, days) {
const expires = new Date();
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
}
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
});
// QR Code functionality (global function for inline onclick handlers)
function showQRCode(url, title) {
console.log('🔍 QR Code button clicked!', url, title);
// Check if QRCode library is available
if (typeof QRCode === 'undefined') {
console.warn('QRCode library not available, using fallback');
showQRCodeFallback(url, title);
return;
}
// Create modal overlay
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
`;
// Create modal content
const modalContent = document.createElement('div');
modalContent.style.cssText = `
background: white;
padding: 30px;
border-radius: 10px;
text-align: center;
max-width: 400px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
`;
// Add title
const titleEl = document.createElement('h3');
titleEl.textContent = `📱 QR Code for ${title}`;
titleEl.style.marginBottom = '20px';
modalContent.appendChild(titleEl);
// Create QR code canvas
const canvas = document.createElement('canvas');
modalContent.appendChild(canvas);
// Generate QR code
QRCode.toCanvas(canvas, url, {
width: 256,
margin: 2,
color: {
dark: '#000000',
light: '#FFFFFF'
}
}, function(error) {
if (error) {
console.error('QR Code generation failed:', error);
modalContent.innerHTML = '<p>❌ Failed to generate QR code</p>';
}
});
// Add close button
const closeButton = document.createElement('button');
closeButton.textContent = '✕ Close';
closeButton.style.cssText = `
margin-top: 20px;
padding: 10px 20px;
background: #f44336;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
`;
closeButton.onclick = () => document.body.removeChild(modal);
modalContent.appendChild(closeButton);
// Add URL text (truncated)
const urlText = document.createElement('p');
urlText.textContent = url.length > 50 ? url.substring(0, 50) + '...' : url;
urlText.style.cssText = `
font-size: 12px;
color: #666;
margin-top: 15px;
word-break: break-all;
`;
modalContent.appendChild(urlText);
modal.appendChild(modalContent);
document.body.appendChild(modal);
// Close on click outside
modal.onclick = (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
}
};
}
// Fallback QR code function using online API
function showQRCodeFallback(url, title) {
console.log('🔍 Using QR code fallback for:', title);
// Create modal overlay
const modal = document.createElement('div');
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
`;
// Create modal content
const modalContent = document.createElement('div');
modalContent.style.cssText = `
background: white;
padding: 30px;
border-radius: 10px;
text-align: center;
max-width: 400px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
`;
// Add title
const titleEl = document.createElement('h3');
titleEl.textContent = `📱 QR Code for ${title}`;
titleEl.style.marginBottom = '20px';
modalContent.appendChild(titleEl);
// Create QR code using API service
const qrImg = document.createElement('img');
const encodedUrl = encodeURIComponent(url);
qrImg.src = `https://api.qrserver.com/v1/create-qr-code/?size=256x256&data=${encodedUrl}`;
qrImg.style.cssText = `
width: 256px;
height: 256px;
border: 2px solid #ddd;
border-radius: 8px;
`;
qrImg.onerror = () => {
qrImg.style.display = 'none';
const errorMsg = document.createElement('p');
errorMsg.textContent = '❌ Failed to generate QR code. You can copy the link instead.';
errorMsg.style.color = 'red';
modalContent.appendChild(errorMsg);
};
modalContent.appendChild(qrImg);
// Add close button
const closeButton = document.createElement('button');
closeButton.textContent = '✕ Close';
closeButton.style.cssText = `
margin-top: 20px;
padding: 10px 20px;
background: #f44336;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
`;
closeButton.onclick = () => document.body.removeChild(modal);
modalContent.appendChild(closeButton);
// Add copy button
const copyButton = document.createElement('button');
copyButton.textContent = '📋 Copy Link';
copyButton.style.cssText = `
margin-top: 10px;
margin-left: 10px;
padding: 10px 20px;
background: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
`;
copyButton.onclick = () => {
navigator.clipboard.writeText(url).then(() => {
copyButton.textContent = '✅ Copied!';
setTimeout(() => copyButton.textContent = '📋 Copy Link', 2000);
});
};
modalContent.appendChild(copyButton);
// Add URL text (truncated)
const urlText = document.createElement('p');
urlText.textContent = url.length > 50 ? url.substring(0, 50) + '...' : url;
urlText.style.cssText = `
font-size: 12px;
color: #666;
margin-top: 15px;
word-break: break-all;
`;
modalContent.appendChild(urlText);
modal.appendChild(modalContent);
document.body.appendChild(modal);
// Close on click outside
modal.onclick = (e) => {
if (e.target === modal) {
document.body.removeChild(modal);
}
};
}

1579
src/static/style.css Normal file

File diff suppressed because it is too large Load Diff

345
src/storage.py Normal file
View File

@@ -0,0 +1,345 @@
"""
Storage abstraction layer for Sharey
Supports multiple storage backends: local filesystem and Backblaze B2
"""
import json
import os
import shutil
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Optional, BinaryIO
from b2sdk.v2 import InMemoryAccountInfo, B2Api
class StorageBackend(ABC):
"""Abstract base class for storage backends"""
@abstractmethod
def upload_file(self, file_content: bytes, file_path: str, content_type: str = None, metadata: Optional[dict] = None) -> bool:
"""Upload a file to storage with optional metadata"""
pass
@abstractmethod
def download_file(self, file_path: str) -> bytes:
"""Download a file from storage"""
pass
@abstractmethod
def delete_file(self, file_path: str) -> bool:
"""Delete a file from storage"""
pass
@abstractmethod
def file_exists(self, file_path: str) -> bool:
"""Check if a file exists in storage"""
pass
@abstractmethod
def get_file_size(self, file_path: str) -> Optional[int]:
"""Get file size in bytes"""
pass
@abstractmethod
def list_files(self, prefix: str = "") -> list:
"""List files with optional prefix"""
pass
@abstractmethod
def get_metadata(self, file_path: str) -> Optional[dict]:
"""Get file metadata"""
pass
class LocalStorageBackend(StorageBackend):
"""Local filesystem storage backend"""
def __init__(self, base_path: str = "storage"):
self.base_path = Path(base_path)
self.base_path.mkdir(exist_ok=True)
print(f"✅ Local storage initialized at: {self.base_path.absolute()}")
def _get_full_path(self, file_path: str) -> Path:
"""Get full filesystem path for a file"""
return self.base_path / file_path
def upload_file(self, file_content: bytes, file_path: str, content_type: str = None, metadata: Optional[dict] = None) -> bool:
"""Upload a file to local storage with optional metadata"""
try:
full_path = self._get_full_path(file_path)
full_path.parent.mkdir(parents=True, exist_ok=True)
# Write the main file
with open(full_path, 'wb') as f:
f.write(file_content)
# Write metadata to a .meta file if provided
if metadata:
import json
meta_path = full_path.with_suffix(full_path.suffix + '.meta')
with open(meta_path, 'w') as f:
json.dump(metadata, f)
print(f"📄 Metadata saved for: {file_path}")
print(f"✅ Local upload successful: {file_path}")
return True
except Exception as e:
print(f"❌ Local upload failed for {file_path}: {e}")
return False
def download_file(self, file_path: str) -> bytes:
"""Download a file from local storage"""
try:
full_path = self._get_full_path(file_path)
with open(full_path, 'rb') as f:
return f.read()
except Exception as e:
print(f"❌ Local download failed for {file_path}: {e}")
raise FileNotFoundError(f"File not found: {file_path}")
def delete_file(self, file_path: str) -> bool:
"""Delete a file from local storage"""
try:
full_path = self._get_full_path(file_path)
if full_path.exists():
full_path.unlink()
print(f"✅ Local file deleted: {file_path}")
return True
return False
except Exception as e:
print(f"❌ Local delete failed for {file_path}: {e}")
return False
def file_exists(self, file_path: str) -> bool:
"""Check if a file exists in local storage"""
return self._get_full_path(file_path).exists()
def get_file_size(self, file_path: str) -> Optional[int]:
"""Get file size in bytes"""
try:
full_path = self._get_full_path(file_path)
if full_path.exists():
return full_path.stat().st_size
return None
except Exception:
return None
def list_files(self, prefix: str = "") -> list:
"""List files with optional prefix"""
try:
if prefix:
search_path = self.base_path / prefix
if search_path.is_dir():
return [str(p.relative_to(self.base_path)) for p in search_path.rglob("*") if p.is_file()]
else:
# Search for files matching prefix pattern
parent = search_path.parent
if parent.exists():
pattern = search_path.name + "*"
return [str(p.relative_to(self.base_path)) for p in parent.glob(pattern) if p.is_file()]
else:
return [str(p.relative_to(self.base_path)) for p in self.base_path.rglob("*") if p.is_file()]
return []
except Exception as e:
print(f"❌ Local list failed: {e}")
return []
def get_metadata(self, file_path: str) -> Optional[dict]:
"""Get file metadata from local storage"""
try:
meta_path = self._get_full_path(file_path + '.meta')
if meta_path.exists():
with open(meta_path, 'r') as f:
metadata = json.load(f)
return metadata
return None
except Exception as e:
print(f"❌ Local metadata read failed for {file_path}: {e}")
return None
class B2StorageBackend(StorageBackend):
"""Backblaze B2 cloud storage backend"""
def __init__(self, key_id: str, key: str, bucket_name: str):
self.key_id = key_id
self.key = key
self.bucket_name = bucket_name
try:
info = InMemoryAccountInfo()
self.b2_api = B2Api(info)
self.b2_api.authorize_account("production", key_id, key)
self.bucket = self.b2_api.get_bucket_by_name(bucket_name)
print(f"✅ B2 storage initialized with bucket: {bucket_name}")
except Exception as e:
print(f"❌ Failed to initialize B2 storage: {e}")
raise
def upload_file(self, file_content: bytes, file_path: str, content_type: str = None, metadata: Optional[dict] = None) -> bool:
"""Upload a file to B2 storage with optional metadata"""
try:
# Prepare file info (metadata) for B2
file_info = {}
if metadata:
# B2 metadata keys must be strings and values must be strings
for key, value in metadata.items():
file_info[str(key)] = str(value)
self.bucket.upload_bytes(
data_bytes=file_content,
file_name=file_path,
content_type=content_type or 'application/octet-stream',
file_info=file_info
)
print(f"✅ B2 upload successful: {file_path}")
if metadata:
print(f"📄 B2 metadata saved for: {file_path}")
return True
except Exception as e:
print(f"❌ B2 upload failed for {file_path}: {e}")
return False
def download_file(self, file_path: str) -> bytes:
"""Download a file from B2 storage"""
try:
download_response = self.bucket.download_file_by_name(file_path)
return download_response.response.content
except Exception as e:
print(f"❌ B2 download failed for {file_path}: {e}")
raise FileNotFoundError(f"File not found: {file_path}")
def delete_file(self, file_path: str) -> bool:
"""Delete a file from B2 storage"""
try:
file_info = self.bucket.get_file_info_by_name(file_path)
self.bucket.delete_file_version(file_info.id_, file_info.file_name)
print(f"✅ B2 file deleted: {file_path}")
return True
except Exception as e:
print(f"❌ B2 delete failed for {file_path}: {e}")
return False
def file_exists(self, file_path: str) -> bool:
"""Check if a file exists in B2 storage"""
try:
self.bucket.get_file_info_by_name(file_path)
return True
except Exception:
return False
def get_file_size(self, file_path: str) -> Optional[int]:
"""Get file size in bytes"""
try:
file_info = self.bucket.get_file_info_by_name(file_path)
return file_info.size
except Exception:
return None
def list_files(self, prefix: str = "") -> list:
"""List files with optional prefix"""
try:
file_list = []
# Use the basic ls() method without parameters first
for file_version, folder in self.bucket.ls():
if prefix == "" or file_version.file_name.startswith(prefix):
file_list.append(file_version.file_name)
return file_list
except Exception as e:
print(f"❌ B2 list failed: {e}")
return []
def get_metadata(self, file_path: str) -> Optional[dict]:
"""Get file metadata from B2 storage"""
try:
# Get file info from B2
file_info = self.bucket.get_file_info_by_name(file_path)
# B2 stores metadata in file_info attribute
metadata = {}
if hasattr(file_info, 'file_info') and file_info.file_info:
metadata.update(file_info.file_info)
return metadata if metadata else None
except Exception as e:
print(f"❌ B2 metadata read failed for {file_path}: {e}")
return None
class StorageManager:
"""Storage manager that handles different backends"""
def __init__(self, config):
self.config = config
self.backend = self._initialize_backend()
def _initialize_backend(self) -> StorageBackend:
"""Initialize the appropriate storage backend based on configuration"""
storage_config = self.config.get('storage', {})
backend_type = storage_config.get('backend', 'b2').lower()
if backend_type == 'local':
storage_path = storage_config.get('local_path', 'storage')
return LocalStorageBackend(storage_path)
elif backend_type == 'b2':
# Validate B2 configuration
if not self.config.validate_b2_config():
raise ValueError("Invalid B2 configuration")
b2_config = self.config.get_b2_config()
return B2StorageBackend(
key_id=b2_config['key_id'],
key=b2_config['key'],
bucket_name=b2_config['bucket_name']
)
else:
raise ValueError(f"Unknown storage backend: {backend_type}")
def upload_file(self, file_content: bytes, file_path: str, content_type: str = None, metadata: Optional[dict] = None) -> bool:
"""Upload a file using the configured backend with optional metadata"""
return self.backend.upload_file(file_content, file_path, content_type, metadata)
def download_file(self, file_path: str) -> bytes:
"""Download a file using the configured backend"""
return self.backend.download_file(file_path)
def delete_file(self, file_path: str) -> bool:
"""Delete a file using the configured backend"""
return self.backend.delete_file(file_path)
def file_exists(self, file_path: str) -> bool:
"""Check if a file exists using the configured backend"""
return self.backend.file_exists(file_path)
def get_file_size(self, file_path: str) -> Optional[int]:
"""Get file size using the configured backend"""
return self.backend.get_file_size(file_path)
def list_files(self, prefix: str = "") -> list:
"""List files using the configured backend"""
return self.backend.list_files(prefix)
def get_metadata(self, file_path: str) -> Optional[dict]:
"""Get file metadata using the configured backend"""
return self.backend.get_metadata(file_path)
def get_backend_type(self) -> str:
"""Get the current backend type"""
storage_config = self.config.get('storage', {})
return storage_config.get('backend', 'b2').lower()
def get_backend_info(self) -> dict:
"""Get information about the current backend"""
backend_type = self.get_backend_type()
info = {'type': backend_type}
if backend_type == 'local':
storage_config = self.config.get('storage', {})
info['path'] = storage_config.get('local_path', 'storage')
elif backend_type == 'b2':
b2_config = self.config.get_b2_config()
info['bucket'] = b2_config.get('bucket_name', 'unknown')
return info

112
src/templates/404.html Normal file
View File

@@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title or "404 - Not Found" }} - Sharey</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📁</text></svg>">
</head>
<body>
<div class="container">
<div class="header">
<h1>📁 Sharey</h1>
<button class="theme-toggle" id="themeToggle" aria-label="Toggle dark mode">🌙</button>
</div>
<div class="section">
<div class="error-container">
<div class="error-icon"></div>
<h2>{{ title or "Content Not Found" }}</h2>
<p>{{ message or "The requested content could not be found." }}</p>
<div class="error-actions">
<a href="/" class="button">🏠 Go Home</a>
<button onclick="history.back()" class="button secondary">← Go Back</button>
</div>
<div class="error-help">
<h3>What happened?</h3>
<ul>
<li>The file or paste might have been deleted</li>
<li>The URL might be typed incorrectly</li>
<li>The content might have expired</li>
</ul>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='script.js') }}"></script>
<style>
.error-container {
text-align: center;
max-width: 500px;
margin: 0 auto;
padding: 40px 20px;
}
.error-icon {
font-size: 4rem;
margin-bottom: 20px;
}
.error-actions {
margin: 30px 0;
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.button {
display: inline-block;
padding: 12px 24px;
background-color: #4CAF50;
color: white;
text-decoration: none;
border-radius: 8px;
border: none;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #45a049;
}
.button.secondary {
background-color: #666;
}
.button.secondary:hover {
background-color: #555;
}
.error-help {
margin-top: 40px;
text-align: left;
background-color: var(--bg-secondary);
padding: 20px;
border-radius: 8px;
border-left: 4px solid #2196F3;
}
.error-help h3 {
margin-top: 0;
color: #2196F3;
}
.error-help ul {
margin: 10px 0;
padding-left: 20px;
}
.error-help li {
margin: 8px 0;
color: var(--text-secondary);
}
</style>
</body>
</html>

278
src/templates/admin.html Normal file
View File

@@ -0,0 +1,278 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sharey Admin Panel</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<!-- Immediate theme application to prevent flash -->
<script>
(function() {
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
function getSystemTheme() {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
const savedTheme = getCookie('sharey-theme') || 'auto';
let actualTheme;
if (savedTheme === 'auto') {
actualTheme = getSystemTheme();
} else {
actualTheme = savedTheme;
}
if (actualTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
<style>
.admin-container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.admin-header {
text-align: center;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid var(--border-color);
}
.admin-section {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.admin-section h3 {
margin-top: 0;
color: var(--text-primary);
border-bottom: 1px solid var(--border-color);
padding-bottom: 10px;
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.status-online { background-color: #4CAF50; }
.status-maintenance { background-color: #ff9800; }
.status-offline { background-color: #f44336; }
.admin-button {
display: inline-block;
padding: 10px 20px;
background: var(--border-color);
color: white;
text-decoration: none;
border-radius: 6px;
border: none;
cursor: pointer;
margin: 5px;
transition: background-color 0.3s ease;
}
.admin-button:hover {
background: var(--text-secondary);
}
.admin-button.danger {
background: #f44336;
}
.admin-button.danger:hover {
background: #d32f2f;
}
.admin-button.success {
background: #4CAF50;
}
.admin-button.success:hover {
background: #45a049;
}
.admin-input {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-primary);
color: var(--text-primary);
}
.admin-textarea {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--bg-primary);
color: var(--text-primary);
resize: vertical;
min-height: 100px;
}
.status-display {
text-align: center;
margin: 20px 0;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 15px 25px;
border-radius: 8px;
font-size: 1.1rem;
}
.status-badge.status-online {
background: #e8f5e8;
border: 2px solid #4CAF50;
color: #2e7d32;
}
.status-badge.status-maintenance {
background: #fff3e0;
border: 2px solid #ff9800;
color: #f57c00;
}
[data-theme="dark"] .status-badge.status-online {
background: #1e3a1e;
color: #81c784;
}
[data-theme="dark"] .status-badge.status-maintenance {
background: #3e2723;
color: #ffb74d;
}
font-size: 0.9rem;
}
.logout-button {
position: absolute;
top: 20px;
right: 20px;
padding: 8px 16px;
background: #f44336;
color: white;
text-decoration: none;
border-radius: 4px;
font-size: 14px;
}
.logout-button:hover {
background: #d32f2f;
}
</style>
</head>
<body>
<!-- Theme toggle button -->
<button id="themeToggle" class="theme-toggle" title="Switch to dark mode"><EFBFBD></button>
<!-- Logout button -->
<a href="/admin/logout" class="logout-button">🚪 Logout</a>
<div class="container">
<div class="admin-container">
<div class="admin-header">
<h1>🛠️ Sharey Admin Panel</h1>
<p>Manage your Sharey instance</p>
</div>
<!-- Service Status -->
<div class="admin-section">
<h3><EFBFBD> Service Status</h3>
<div class="status-display">
{% if maintenance_enabled %}
<div class="status-badge status-maintenance">
<span class="status-indicator status-maintenance"></span>
<strong>Maintenance Mode</strong>
</div>
{% else %}
<div class="status-badge status-online">
<span class="status-indicator status-online"></span>
<strong>Online</strong>
</div>
{% endif %}
</div>
</div>
<!-- Maintenance Mode -->
<div class="admin-section">
<h3>🔧 Maintenance Mode</h3>
<p><strong>Current Status:</strong>
{% if maintenance_enabled %}
<span style="color: #ff9800;">🟡 ENABLED</span>
{% else %}
<span style="color: #4CAF50;">🟢 DISABLED</span>
{% endif %}
</p>
<form method="POST" action="/admin/maintenance">
<label for="maintenance_message">Maintenance Message:</label>
<textarea name="maintenance_message" id="maintenance_message" class="admin-textarea" placeholder="Enter maintenance message...">{{ maintenance_message }}</textarea>
<label for="estimated_return">Estimated Return Time:</label>
<input type="text" name="estimated_return" id="estimated_return" class="admin-input" placeholder="e.g., 2025-08-22 15:00 UTC" value="{{ estimated_return }}">
{% if maintenance_enabled %}
<button type="submit" name="action" value="disable" class="admin-button success">✅ Disable Maintenance Mode</button>
{% else %}
<button type="submit" name="action" value="enable" class="admin-button danger">🔧 Enable Maintenance Mode</button>
{% endif %}
</form>
</div>
<!-- Configuration -->
<div class="admin-section">
<h3>⚙️ Configuration</h3>
<form method="POST" action="/admin/config">
<label for="max_file_size">Max File Size (MB):</label>
<input type="number" name="max_file_size" id="max_file_size" class="admin-input" value="{{ config.upload.max_file_size_mb }}" min="1" max="1000">
<label for="max_paste_length">Max Paste Length (characters):</label>
<input type="number" name="max_paste_length" id="max_paste_length" class="admin-input" value="{{ config.paste.max_length }}" min="1000" max="10000000">
<button type="submit" class="admin-button">💾 Save Configuration</button>
</form>
</div>
<!-- Quick Actions -->
<div class="admin-section">
<h3>⚡ Quick Actions</h3>
<a href="/admin/stats" class="admin-button">📈 Detailed Statistics</a>
<a href="/health" class="admin-button" target="_blank">🩺 Health Check</a>
<a href="/" class="admin-button" target="_blank">🏠 View Site</a>
<button onclick="location.reload()" class="admin-button">🔄 Refresh Data</button>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>

View File

@@ -0,0 +1,196 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Share <!-- Theme toggle button -->
<button id="themeToggle" class="theme-toggle" title="Switch to dark mode">🌙</button>Admin Login</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<!-- Immediate theme application to prevent flash -->
<script>
(function() {
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
function getSystemTheme() {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
const savedTheme = getCookie('sharey-theme') || 'auto';
let actualTheme;
if (savedTheme === 'auto') {
actualTheme = getSystemTheme();
} else {
actualTheme = savedTheme;
}
if (actualTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
<style>
.login-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 80vh;
padding: 20px;
}
.login-box {
background: var(--bg-secondary);
border: 2px solid var(--border-color);
border-radius: 12px;
padding: 40px;
max-width: 400px;
width: 100%;
text-align: center;
box-shadow: 0 4px 20px var(--shadow);
}
.login-header {
margin-bottom: 30px;
}
.login-icon {
font-size: 3rem;
margin-bottom: 15px;
color: var(--border-color);
}
.login-title {
color: var(--text-primary);
margin: 0 0 10px 0;
font-size: 1.8rem;
}
.login-subtitle {
color: var(--text-secondary);
margin: 0;
font-size: 1rem;
}
.login-form {
text-align: left;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
display: block;
margin-bottom: 8px;
color: var(--text-primary);
font-weight: bold;
}
.form-input {
width: 100%;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 16px;
box-sizing: border-box;
transition: border-color 0.3s ease;
}
.form-input:focus {
outline: none;
border-color: var(--text-secondary);
}
.login-button {
width: 100%;
padding: 12px;
background: var(--border-color);
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s ease;
}
.login-button:hover {
background: var(--text-secondary);
}
.error-message {
background: #f44336;
color: white;
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
text-align: center;
}
.back-link {
margin-top: 20px;
text-align: center;
}
.back-link a {
color: var(--text-secondary);
text-decoration: none;
font-size: 14px;
}
.back-link a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<!-- Theme toggle button -->
<button id="themeToggle" class="theme-toggle" title="Switch to dark mode"><EFBFBD></button>
<div class="container">
<div class="login-container">
<div class="login-box">
<div class="login-header">
<div class="login-icon">🔐</div>
<h1 class="login-title">Admin Access</h1>
<p class="login-subtitle">Enter password to access admin panel</p>
</div>
{% if error %}
<div class="error-message">
{{ error }}
</div>
{% endif %}
<form method="POST" class="login-form">
<div class="form-group">
<label for="password" class="form-label">Password:</label>
<input type="password" id="password" name="password" class="form-input" required autocomplete="current-password">
</div>
<button type="submit" class="login-button">🚪 Login</button>
</form>
<div class="back-link">
<a href="/">← Back to Sharey</a>
</div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>

288
src/templates/index.html Normal file
View File

@@ -0,0 +1,288 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<title>Sharey</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<!-- QR Code generation library -->
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<!-- Immediate theme application to prevent flash -->
<script>
// Apply theme immediately before page renders
(function() {
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
const savedTheme = getCookie('sharey-theme') || 'light';
if (savedTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
</head>
<body>
<!-- Theme toggle button -->
<button id="themeToggle" class="theme-toggle" title="Switch to dark mode">🌙</button>
<div class="container">
<h1>Sharey~</h1>
<!-- Toggle button to switch between modes -->
<div class="toggle-container">
<button id="fileMode" class="toggle-button active">File Sharing</button>
<button id="pasteMode" class="toggle-button">Pastebin</button>
<button id="urlMode" class="toggle-button">URL Shortener</button>
<button id="faqButton" class="toggle-button">FAQ</button>
<!-- File Expiry Selection (inline) -->
<div class="expiry-inline">
<label for="expirySelect" class="expiry-label"></label>
<select id="expirySelect" class="expiry-select-inline">
<option value="never">Never</option>
<option value="1h">1h</option>
<option value="24h">24h</option>
<option value="7d" selected>7d</option>
<option value="30d">30d</option>
<option value="90d">90d</option>
<option value="custom">Custom</option>
</select>
<input type="datetime-local" id="customExpiry" class="custom-expiry-inline" style="display: none;">
</div>
</div>
<!-- File Sharing Area -->
<div id="fileSharingSection" class="section">
<div id="dropArea" class="drop-area">
<div class="upload-icon">📁</div>
<p id="desktopInstructions" class="desktop-only">Drop files here, click to select, or press <kbd>Ctrl+V</kbd> to paste from clipboard</p>
<p id="mobileInstructions" class="mobile-only" style="display: none;">📱 Tap to select files or long-press for paste menu</p>
<input type="file" id="fileInput" multiple accept="*/*">
</div>
<!-- Mobile clipboard paste button (fallback) -->
<div id="mobileClipboardSection" class="mobile-clipboard-section" style="display: none;">
<button id="mobilePasteButton" class="mobile-paste-button">📋 Paste from Clipboard</button>
<p class="mobile-paste-hint">Fallback option for devices that don't support long-press</p>
</div>
<!-- Context menu for long-press -->
<div id="contextMenu" class="context-menu" style="display: none;">
<button id="contextPasteButton" class="context-menu-item">📋 Paste from Clipboard</button>
<button id="contextSelectButton" class="context-menu-item">📁 Select Files</button>
</div>
<div id="progressContainer" class="progress-bar">
<div id="progressBar" class="progress"></div>
<p id="progressText">Ready to upload</p>
</div>
<div id="result" class="result-message"></div>
<!-- File preview container -->
<div id="filePreviewContainer" class="file-preview-container" style="display: none;">
<h3>File Previews:</h3>
<div id="filePreviews" class="file-previews"></div>
</div>
</div>
<!-- Pastebin Area (hidden by default) -->
<div id="pastebinSection" class="section" style="display: none;">
<textarea id="pasteContent" placeholder="Enter your text here..." rows="10" cols="50"></textarea>
<!-- Paste Expiry Selection -->
<div class="expiry-section">
<label for="pasteExpirySelect" class="expiry-label">⏰ Paste Expiry:</label>
<select id="pasteExpirySelect" class="expiry-select">
<option value="never">Never expires</option>
<option value="1h">1 Hour</option>
<option value="24h" selected>24 Hours</option>
<option value="7d">7 Days</option>
<option value="30d">30 Days</option>
<option value="custom">Custom...</option>
</select>
<input type="datetime-local" id="customPasteExpiry" class="custom-expiry" style="display: none;">
<p class="expiry-hint">Pastes will be automatically deleted after the expiry time</p>
</div>
<button id="submitPaste" class="submit-button">Submit Paste</button>
<div id="pasteResult" class="result-message"></div>
</div>
<!-- URL Shortener Area (hidden by default) -->
<div id="urlShortenerSection" class="section" style="display: none;">
<div class="url-input-container">
<label for="urlInput" class="url-label">🔗 Enter URL to shorten:</label>
<input type="url" id="urlInput" class="url-input" placeholder="https://example.com/very-long-url" required>
<div class="url-options">
<div class="option-group">
<label for="customCode" class="option-label">Custom code (optional):</label>
<input type="text" id="customCode" class="custom-code-input" placeholder="my-link" maxlength="20">
<small class="option-hint">3-20 characters, letters, numbers, hyphens, underscores only</small>
</div>
<div class="option-group">
<label for="urlExpiry" class="option-label">⏰ Expires in:</label>
<select id="urlExpiry" class="url-expiry-select">
<option value="never">Never</option>
<option value="1">1 Hour</option>
<option value="24" selected>24 Hours</option>
<option value="168">7 Days</option>
<option value="720">30 Days</option>
</select>
</div>
</div>
<button id="shortenUrl" class="submit-button">✨ Shorten URL</button>
</div>
<div id="urlResult" class="result-message"></div>
<!-- URL Preview Container -->
<div id="urlPreviewContainer" class="url-preview-container" style="display: none;">
<h3>🔗 Short URL Created:</h3>
<div id="urlPreview" class="url-preview">
<div class="short-url-display">
<input type="text" id="shortUrlInput" class="short-url-input" readonly>
<button id="copyUrlButton" class="copy-button" title="Copy to clipboard">📋</button>
</div>
<div class="url-details">
<p><strong>Target:</strong> <span id="targetUrl"></span></p>
<p><strong>Clicks:</strong> <span id="clickCount">0</span></p>
<p id="expiryInfo" style="display: none;"><strong>Expires:</strong> <span id="expiryDate"></span></p>
</div>
</div>
</div>
</div>
<!-- FAQ Section (hidden by default) -->
<div id="faqSection" class="section" style="display: none;">
<h2>Terms of Service & Legal</h2>
<!-- Terms of Service Section -->
<div class="faq-section" style="border: 2px solid var(--border-color); background-color: var(--bg-accent); margin-bottom: 25px;">
<h3>📋 Terms of Service</h3>
<p><strong>By using Sharey, you agree to these terms:</strong></p>
<ul>
<li><strong>Content Responsibility:</strong> You are solely responsible for all content you upload and URLs you shorten. Sharey does not review, monitor, or control user content or redirect destinations.</li>
<li><strong>Prohibited Content:</strong> Do not upload illegal, copyrighted, harmful, or inappropriate content including malware, spam, harassment, or content violating applicable laws. Do not create redirects to illegal, malicious, or harmful websites.</li>
<li><strong>Takedown Policy:</strong> We respond promptly to valid takedown requests. To report content or malicious redirects, email: <strong>computertech@rizon.net</strong></li>
<li><strong>Service Disclaimer:</strong> Sharey provides file hosting and URL shortening "as-is" without warranties. We reserve the right to remove content, disable redirects, and suspend access at our discretion.</li>
<li><strong>Privacy:</strong> Files may expire automatically. Shortened URLs may expire based on settings. We do not access private content except for legal compliance or security purposes.</li>
<li><strong>User Indemnification:</strong> You agree to indemnify Sharey against any claims arising from your use, content, or redirect destinations.</li>
</ul>
<p><strong>🚨 Report Abuse:</strong> Email <a href="mailto:computertech@rizon.net">computertech@rizon.net</a> with the file URL and reason for reporting.</p>
<p><em>Last updated: September 2025</em></p>
</div>
<h2>Frequently Asked Questions</h2>
<div class="faq-section">
<h3>How do I upload a file?</h3>
<p>To upload a file, send a POST request to the /api/upload endpoint. Example:</p>
<pre><code>curl -X POST https://sharey.org/api/upload \
-F "files[]=@/path/to/your/file.txt"</code></pre>
</div>
<div class="faq-section">
<h3>How do I create a paste?</h3>
<p>To create a paste, send a POST request to the /api/paste endpoint. Example:</p>
<pre><code>curl -X POST https://sharey.org/api/paste \
-H "Content-Type: application/json" \
-d '{"content": "This is the content of my paste."}'</code></pre>
</div>
<div class="faq-section">
<h3>🆕 Can I upload from clipboard?</h3>
<p>Yes! You can paste content directly from your clipboard:</p>
<ul class="desktop-only">
<li><strong>Keyboard:</strong> Press <kbd>Ctrl+V</kbd> (or <kbd>Cmd+V</kbd> on Mac) in File Sharing mode</li>
<li><strong>Images:</strong> Screenshots, copied images, photos - all work!</li>
<li><strong>Text:</strong> Copied text will prompt you to switch to Paste mode</li>
</ul>
<ul class="mobile-only" style="display: none;">
<li><strong>Long-press:</strong> Long-press the upload area and select "Paste from Clipboard"</li>
<li><strong>Screenshots:</strong> Take a screenshot, then long-press to paste</li>
<li><strong>Copied images:</strong> Copy any image, then long-press to paste</li>
<li><strong>Text content:</strong> Will prompt you to switch to Paste mode</li>
</ul>
<p><em>Perfect for quickly sharing screenshots, images from other websites, or text content!</em></p>
<div class="mobile-tutorial mobile-only" style="display: none;">
<h4>📱 Mobile Step-by-Step:</h4>
<ol>
<li>Copy an image or take a screenshot</li>
<li>Come to this page in File Sharing mode</li>
<li>Long-press the upload area</li>
<li>Select "📋 Paste from Clipboard" from the menu</li>
<li>Your content will be uploaded automatically!</li>
</ol>
</div>
<div class="desktop-tutorial desktop-only">
<h4>💻 Desktop Step-by-Step:</h4>
<ol>
<li>Copy an image or take a screenshot (<kbd>PrtScr</kbd>, <kbd>Cmd+Shift+4</kbd>, etc.)</li>
<li>Come to this page in File Sharing mode</li>
<li>Press <kbd>Ctrl+V</kbd> (or <kbd>Cmd+V</kbd> on Mac)</li>
<li>Your content will be uploaded automatically!</li>
</ol>
</div>
</div>
<div class="faq-section">
<h3>🆕 What's the URL format?</h3>
<p>Sharey now uses clean, short URLs for all new uploads:</p>
<ul>
<li><strong>New format:</strong> <code>sharey.org/ABC123</code> (files) or <code>sharey.org/XYZ789</code> (pastes)</li>
<li><strong>Old format:</strong> <code>sharey.org/files/ABC123.png</code> or <code>sharey.org/pastes/XYZ789</code> (still works!)</li>
</ul>
<p><em>All existing links continue to work - only new uploads use the cleaner format.</em></p>
</div>
<div class="faq-section">
<h3>⏰ Do files expire?</h3>
<p>Yes! You can set expiry times when uploading:</p>
<ul>
<li><strong>Files:</strong> Default 7 days, options from 1 hour to never</li>
<li><strong>Pastes:</strong> Default 24 hours, configurable expiry</li>
<li><strong>Custom expiry:</strong> Set any specific date and time</li>
<li><strong>Automatic cleanup:</strong> Expired content is automatically deleted</li>
</ul>
<p><em>Perfect for temporary shares or sensitive content that shouldn't persist!</em></p>
</div>
<div class="faq-section">
<h3>🔗 How do I shorten a URL?</h3>
<p>Use the URL Shortener tab to create short links:</p>
<ul>
<li><strong>Basic shortening:</strong> Enter any URL and get a short link like <code>sharey.org/abc123</code></li>
<li><strong>Custom codes:</strong> Create memorable links like <code>sharey.org/my-project</code></li>
<li><strong>Expiring links:</strong> Set links to expire automatically for security</li>
<li><strong>Click tracking:</strong> See how many times your link has been clicked</li>
</ul>
<p><strong>API Usage:</strong></p>
<pre><code>curl -X POST https://sharey.org/api/shorten \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com", "code": "my-link", "expires_in_hours": 24}'</code></pre>
<p><em>Perfect for sharing long URLs, tracking clicks, or creating temporary access links!</em></p>
</div>
</div>
</div>
<!-- QR Code library for sharing -->
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>

View File

@@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="en">
< <!-- Theme toggle button -->
<button id="themeToggle" class="theme-toggle" title="Switch to dark mode">🌙</button>ad>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sharey</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<!-- QR Code generation library -->
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<!-- Immediate theme application to prevent flash -->
<script>
// Apply theme immediately before page renders
(function() {
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
const savedTheme = getCookie('sharey-theme') || 'light';
if (savedTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
</head>
<body>
<!-- Theme toggle button -->
<button id="themeToggle" class="theme-toggle" title="Switch to dark mode"><EFBFBD></button>
<div class="container">
<h1>Sharey~</h1>
<!-- Toggle button to switch between modes -->
<div class="toggle-container">
<button id="fileMode" class="toggle-button active">File Sharing</button>
<button id="pasteMode" class="toggle-button">Pastebin</button>
<button id="faqButton" class="toggle-button">FAQ</button> <!-- Updated FAQ button -->
</div>
<!-- File Sharing Area -->
<div id="fileSharingSection" class="section">
<div id="drop-area" class="drop-area">
<p>Drag & Drop Files Here or Click to Select</p>
<input type="file" id="fileInput" multiple style="display: none;">
</div>
<div id="progressContainer" class="progress-bar" style="display: none;">
<div id="progressBar" class="progress"></div>
</div>
<p id="progressText">0%</p>
<div id="result" class="result-message"></div>
<!-- File preview container -->
<div id="filePreviewContainer" class="file-preview-container" style="display: none;">
<h3>File Previews:</h3>
<div id="filePreviews" class="file-previews"></div>
</div>
</div>
<!-- Pastebin Area (hidden by default) -->
<div id="pastebinSection" class="section" style="display: none;">
<textarea id="pasteContent" placeholder="Enter your text here..." rows="10" cols="50"></textarea>
<button id="submitPaste" class="submit-button">Submit Paste</button>
<div id="pasteResult" class="result-message"></div>
</div>
<!-- FAQ Section (hidden by default) -->
<div id="faqSection" class="section" style="display: none;">
<h2>Frequently Asked Questions</h2>
<div class="faq-section">
<h3>How do I upload a file?</h3>
<p>To upload a file, send a POST request to the /api/upload endpoint. Example:</p>
<pre><code>curl -X POST https://sharey.org/api/upload \
-F "files[]=@/path/to/your/file.txt"</code></pre>
</div>
<div class="faq-section">
<h3>How do I create a paste?</h3>
<p>To create a paste, send a POST request to the /api/paste endpoint. Example:</p>
<pre><code>curl -X POST https://sharey.org/api/paste \
-H "Content-Type: application/json" \
-d '{"content": "This is the content of my paste."}'</code></pre>
</div>
<!-- Add more FAQ items as needed -->
</div>
</div>
<!-- QR Code library for sharing -->
<script src="https://cdn.jsdelivr.net/npm/qrcode@1.5.3/build/qrcode.min.js"></script>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>

View File

@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="wi <!-- Theme toggle button -->
<button id="themeToggle" class="theme-toggle" title="Switch to dark mode">🌙</button>h=device-width, initial-scale=1.0">
<title>Sharey - Maintenance</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<!-- Immediate theme application to prevent flash -->
<script>
(function() {
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
function getSystemTheme() {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
const savedTheme = getCookie('sharey-theme') || 'auto';
let actualTheme;
if (savedTheme === 'auto') {
actualTheme = getSystemTheme();
} else {
actualTheme = savedTheme;
}
if (actualTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
<style>
.maintenance-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 80vh;
text-align: center;
padding: 20px;
}
.maintenance-icon {
font-size: 4rem;
margin-bottom: 20px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 1; }
50% { opacity: 0.5; }
100% { opacity: 1; }
}
.maintenance-title {
font-size: 2.5rem;
color: var(--text-primary);
margin-bottom: 20px;
}
.maintenance-message {
font-size: 1.2rem;
color: var(--text-secondary);
margin-bottom: 30px;
max-width: 600px;
line-height: 1.6;
}
.maintenance-return {
font-size: 1rem;
color: var(--text-secondary);
background: var(--bg-secondary);
padding: 15px;
border-radius: 8px;
border: 1px solid var(--border-color);
}
.refresh-button {
display: inline-block;
margin-top: 20px;
padding: 12px 24px;
background: var(--border-color);
color: white;
text-decoration: none;
border-radius: 6px;
transition: background-color 0.3s ease;
}
.refresh-button:hover {
background: var(--text-secondary);
}
</style>
</head>
<body>
<!-- Theme toggle button -->
<button id="themeToggle" class="theme-toggle" title="Switch to dark mode"><EFBFBD></button>
<div class="container">
<div class="maintenance-container">
<div class="maintenance-icon">🔧</div>
<h1 class="maintenance-title">Under Maintenance</h1>
<p class="maintenance-message">{{ message }}</p>
{% if estimated_return %}
<div class="maintenance-return">
<strong>Estimated return:</strong> {{ estimated_return }}
</div>
{% endif %}
<a href="/" class="refresh-button">🔄 Check Again</a>
</div>
</div>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</body>
</html>

View File

@@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<title>File - Sharey</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<!-- Immediate theme application to prevent flash -->
<script>
// Apply theme immediately before page renders
(function() {
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
const savedTheme = getCookie('sharey-theme') || 'light';
if (savedTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
</head>
<body>
<!-- Theme toggle button -->
<button id="themeToggle" class="theme-toggle" title="Switch to dark mode">🌙</button>
<div class="container">
<h1>📁 File View</h1>
<div class="file-container">
<div class="file-header">
<h3>{{ filename }}</h3>
<div class="file-actions">
<a href="{{ download_url }}" class="download-button" download>💾 Download</a>
<a href="/" class="home-button">🏠 Home</a>
</div>
</div>
<div class="file-info">
<p><strong>Size:</strong> {{ file_size }}</p>
<p><strong>Type:</strong> {{ file_type }}</p>
</div>
{% if is_viewable %}
<div class="file-preview">
{% if file_type.startswith('image/') %}
<img src="{{ download_url }}" alt="{{ filename }}" style="max-width: 100%; height: auto;">
{% elif file_type.startswith('text/') or file_type == 'application/json' %}
<pre><code>{{ file_content | e }}</code></pre>
{% else %}
<p>Preview not available for this file type.</p>
{% endif %}
</div>
{% else %}
<div class="file-preview">
<p>📄 This file cannot be previewed. Click download to view it.</p>
</div>
{% endif %}
</div>
</div>
<script>
// Theme management
function initializeTheme() {
const savedTheme = getCookie('sharey-theme') || 'light';
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
themeToggle.addEventListener('click', toggleTheme);
updateThemeButton(savedTheme);
}
}
function toggleTheme() {
const currentTheme = getCookie('sharey-theme') || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
applyTheme(newTheme);
setCookie('sharey-theme', newTheme, 365);
updateThemeButton(newTheme);
}
function applyTheme(themeSetting) {
if (themeSetting === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.removeAttribute('data-theme');
}
}
function updateThemeButton(themeSetting) {
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
if (themeSetting === 'light') {
themeToggle.textContent = '🌙';
themeToggle.title = 'Switch to dark mode';
} else {
themeToggle.textContent = '☀️';
themeToggle.title = 'Switch to light mode';
}
}
}
function setCookie(name, value, days) {
const expires = new Date();
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
}
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
// Initialize theme on page load
document.addEventListener('DOMContentLoaded', initializeTheme);
</script>
</body>
</html>

View File

@@ -0,0 +1,134 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<title>Paste - Sharey</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<!-- Immediate theme application to prevent flash -->
<script>
// Apply theme immediately before page renders
(function() {
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
const savedTheme = getCookie('sharey-theme') || 'light';
if (savedTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
</head>
<body>
<!-- Theme toggle button -->
<button id="themeToggle" class="theme-toggle" title="Switch to dark mode">🌙</button>
<div class="container">
<div class="paste-container">
<div class="paste-header">
<h3>Paste Content</h3>
<div class="paste-actions">
<button class="copy-button" onclick="copyToClipboard()">📋 Copy</button>
<a href="/pastes/raw/{{ paste_id }}" class="raw-button" target="_blank">📄 Raw</a>
<a href="/" class="home-button">🏠 Home</a>
</div>
</div>
<div class="paste-content">
<pre><code>{{ content | e }}</code></pre>
</div>
</div>
</div>
<script>
function copyToClipboard() {
const content = document.querySelector('.paste-content code').textContent;
navigator.clipboard.writeText(content).then(() => {
const button = document.querySelector('.copy-button');
const originalText = button.textContent;
button.textContent = '✅ Copied!';
setTimeout(() => {
button.textContent = originalText;
}, 2000);
}).catch(err => {
console.error('Failed to copy: ', err);
alert('Failed to copy to clipboard');
});
}
// Theme management
function initializeTheme() {
const savedTheme = getCookie('sharey-theme') || 'light';
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
themeToggle.addEventListener('click', toggleTheme);
updateThemeButton(savedTheme);
}
}
function toggleTheme() {
const currentTheme = getCookie('sharey-theme') || 'light';
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
applyTheme(newTheme);
setCookie('sharey-theme', newTheme, 365);
updateThemeButton(newTheme);
}
function applyTheme(themeSetting) {
if (themeSetting === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.removeAttribute('data-theme');
}
}
function updateThemeButton(themeSetting) {
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
if (themeSetting === 'light') {
themeToggle.textContent = '🌙';
themeToggle.title = 'Switch to dark mode';
} else {
themeToggle.textContent = '☀️';
themeToggle.title = 'Switch to light mode';
}
}
}
function setCookie(name, value, days) {
const expires = new Date();
expires.setTime(expires.getTime() + (days * 24 * 60 * 60 * 1000));
document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/;SameSite=Lax`;
}
function getCookie(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
// Initialize theme on page load
document.addEventListener('DOMContentLoaded', initializeTheme);
</script>
</body>
</html>

63
start_production.sh Normal file
View File

@@ -0,0 +1,63 @@
#!/bin/bash
# Production startup script for Sharey with Gunicorn
# Usage: ./start_production.sh
set -e
echo "🚀 Starting Sharey in Production Mode with Gunicorn"
echo "=================================================="
# Check if we're in the right directory
if [ ! -f "wsgi.py" ]; then
echo "❌ Error: wsgi.py not found. Please run this script from the Sharey root directory."
exit 1
fi
# Check if virtual environment exists
if [ ! -d ".venv" ]; then
echo "❌ Virtual environment not found. Please run setup first."
exit 1
fi
# Activate virtual environment
echo "🔌 Activating virtual environment..."
source .venv/bin/activate
# Check if config exists
if [ ! -f "config.json" ]; then
echo "⚠️ Warning: config.json not found. Creating from example..."
cp config.json.example config.json
echo "✏️ Please edit config.json with your settings and run again."
exit 1
fi
# Create logs directory
mkdir -p logs
# Validate configuration
echo "🔧 Validating configuration..."
python src/config_util.py validate || {
echo "❌ Configuration validation failed. Please fix your config.json"
exit 1
}
# Test storage backend
echo "🧪 Testing storage backend..."
python test_storage.py || {
echo "❌ Storage test failed. Please check your configuration."
exit 1
}
echo "✅ All checks passed!"
echo ""
echo "🌟 Starting Gunicorn server..."
echo "🌐 Server will be available at: http://0.0.0.0:8866"
echo "📁 Working directory: $(pwd)"
echo "📊 Workers: $(python -c 'import multiprocessing; print(multiprocessing.cpu_count() * 2 + 1)')"
echo "📝 Logs: logs/gunicorn_access.log, logs/gunicorn_error.log"
echo ""
echo "Press Ctrl+C to stop"
echo "=================================================="
# Start Gunicorn
exec gunicorn --config gunicorn.conf.py wsgi:application

BIN
storage/files/GAo01R.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View File

@@ -0,0 +1 @@
{"uploaded_at": "2025-09-08T19:42:27.845206", "original_name": "Screenshot from 2025-09-08 18-31-10.png", "content_type": "image/png", "expires_at": "2025-09-08T18:43:00.000Z"}

BIN
storage/files/SDlrhr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 KiB

View File

@@ -0,0 +1 @@
{"uploaded_at": "2025-09-08T19:42:11.115834", "original_name": "Screenshot from 2025-09-08 18-47-41.png", "content_type": "image/png"}

3
test_expiry.txt Normal file
View File

@@ -0,0 +1,3 @@
This is a test file for expiry testing.
Created: 2025-09-08T19:35:21
Content: Test expiry functionality

111
test_storage.py Normal file
View File

@@ -0,0 +1,111 @@
#!/usr/bin/env python3
"""
Test script for storage backends
"""
import sys
import os
from pathlib import Path
# Add src directory to Python path
sys.path.insert(0, str(Path(__file__).parent / "src"))
from config import config
from storage import StorageManager
def test_storage():
"""Test the storage backend"""
print("🧪 Testing storage backend...")
try:
# Initialize storage manager
storage = StorageManager(config)
backend_info = storage.get_backend_info()
print(f"✅ Storage backend initialized: {backend_info['type']}")
if backend_info['type'] == 'b2':
print(f" → B2 bucket: {backend_info.get('bucket', 'unknown')}")
elif backend_info['type'] == 'local':
print(f" → Local path: {backend_info.get('path', 'unknown')}")
# Test file upload
test_content = b"Hello, this is a test file for storage backend testing!"
test_file_path = "test/test_file.txt"
print(f"\n📤 Testing file upload...")
success = storage.upload_file(test_content, test_file_path, "text/plain")
if success:
print(f"✅ Upload successful: {test_file_path}")
# Test file download
print(f"📥 Testing file download...")
downloaded_content = storage.download_file(test_file_path)
if downloaded_content == test_content:
print(f"✅ Download successful and content matches")
# Test file exists
print(f"🔍 Testing file exists...")
exists = storage.file_exists(test_file_path)
print(f"✅ File exists: {exists}")
# Test file size
print(f"📏 Testing file size...")
size = storage.get_file_size(test_file_path)
print(f"✅ File size: {size} bytes")
# Test file listing
print(f"📋 Testing file listing...")
files = storage.list_files("test/")
print(f"✅ Files in test/ folder: {len(files)} files")
for file in files:
print(f"{file}")
# Test cleanup
print(f"🗑️ Testing file deletion...")
deleted = storage.delete_file(test_file_path)
print(f"✅ File deleted: {deleted}")
else:
print(f"❌ Downloaded content doesn't match original")
return False
else:
print(f"❌ Upload failed")
return False
print(f"\n🎉 All storage tests passed!")
return True
except Exception as e:
print(f"❌ Storage test failed: {e}")
return False
def main():
print("🚀 Sharey Storage Backend Test")
print("=" * 40)
# Show current configuration
print("\n📋 Current Storage Configuration:")
storage_config = config.get_storage_config()
print(f" Backend: {storage_config.get('backend', 'unknown')}")
if storage_config.get('backend') == 'local':
print(f" Local Path: {storage_config.get('local_path', 'storage')}")
elif storage_config.get('backend') == 'b2':
if config.validate_b2_config():
b2_config = config.get_b2_config()
print(f" B2 Bucket: {b2_config.get('bucket_name', 'unknown')}")
else:
print(" B2 Configuration: Invalid")
# Test the storage backend
if test_storage():
print("\n✅ Storage backend is working correctly!")
sys.exit(0)
else:
print("\n❌ Storage backend test failed!")
sys.exit(1)
if __name__ == "__main__":
main()

22
wsgi.py Normal file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python3
"""
WSGI entry point for Sharey
This file is used by Gunicorn and other WSGI servers
"""
import sys
import os
from pathlib import Path
# Add src directory to Python path
src_path = Path(__file__).parent / "src"
sys.path.insert(0, str(src_path))
# Import the Flask application
from app import app
# Create the WSGI application
application = app
if __name__ == "__main__":
# This allows running the WSGI file directly for testing
app.run(host="0.0.0.0", port=8866, debug=False)