Uploaded code
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal 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
67
MAINTENANCE.md
Normal 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
237
README.md
Normal 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
73
STORAGE_QUICKSTART.md
Normal 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)
|
||||
BIN
__pycache__/expiry_db.cpython-312.pyc
Normal file
BIN
__pycache__/expiry_db.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/gunicorn.conf.cpython-312.pyc
Normal file
BIN
__pycache__/gunicorn.conf.cpython-312.pyc
Normal file
Binary file not shown.
BIN
__pycache__/wsgi.cpython-312.pyc
Normal file
BIN
__pycache__/wsgi.cpython-312.pyc
Normal file
Binary file not shown.
131
cleanup_daemon.py
Executable file
131
cleanup_daemon.py
Executable 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
154
cleanup_expired.py
Executable 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
88
cleanup_expired_db.py
Executable 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
36
config.json
Normal 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
44
config.json.example
Normal 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
15
cron_example.txt
Normal 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
147
docs/CONFIG_SYSTEM.md
Normal 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
79
docs/DEPLOYMENT.md
Normal 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
67
docs/MAINTENANCE.md
Normal 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
118
docs/MIGRATION_SUMMARY.md
Normal 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
99
docs/ORGANIZATION.md
Normal 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
196
docs/README.md
Normal 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
375
docs/STORAGE.md
Normal 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).
|
||||
306
expiry_db.py
Normal file
306
expiry_db.py
Normal 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
70
gunicorn.conf.py
Normal 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
122
horizontal-layout.js
Normal 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
107
production.py
Normal 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
5
requirements.txt
Normal 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
25
scripts/clean.sh
Executable 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
35
scripts/config.json
Normal 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
450
scripts/migrate.py
Normal 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()
|
||||
51
scripts/set_admin_password.py
Normal file
51
scripts/set_admin_password.py
Normal 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
280
scripts/setup.py
Normal 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
51
set_admin_password.py
Normal 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
57
sharey
Executable 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
303
sharey.py
Normal 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()
|
||||
BIN
src/__pycache__/app.cpython-312.pyc
Normal file
BIN
src/__pycache__/app.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/config.cpython-312.pyc
Normal file
BIN
src/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
src/__pycache__/storage.cpython-312.pyc
Normal file
BIN
src/__pycache__/storage.cpython-312.pyc
Normal file
Binary file not shown.
695
src/app.py
Normal file
695
src/app.py
Normal 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
187
src/config.py
Normal 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
347
src/config_util.py
Normal 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()
|
||||
39
src/static/image-preview.js
Normal file
39
src/static/image-preview.js
Normal 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
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
619
src/static/script_backup.js
Normal 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
1579
src/static/style.css
Normal file
File diff suppressed because it is too large
Load Diff
345
src/storage.py
Normal file
345
src/storage.py
Normal 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
112
src/templates/404.html
Normal 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
278
src/templates/admin.html
Normal 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>
|
||||
196
src/templates/admin_login.html
Normal file
196
src/templates/admin_login.html
Normal 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
288
src/templates/index.html
Normal 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>
|
||||
102
src/templates/index_backup.html
Normal file
102
src/templates/index_backup.html
Normal 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>
|
||||
127
src/templates/maintenance.html
Normal file
127
src/templates/maintenance.html
Normal 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>
|
||||
137
src/templates/view_file.html
Normal file
137
src/templates/view_file.html
Normal 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>
|
||||
134
src/templates/view_paste.html
Normal file
134
src/templates/view_paste.html
Normal 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
63
start_production.sh
Normal 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
BIN
storage/files/GAo01R.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
1
storage/files/GAo01R.png.meta
Normal file
1
storage/files/GAo01R.png.meta
Normal 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
BIN
storage/files/SDlrhr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 736 KiB |
1
storage/files/SDlrhr.png.meta
Normal file
1
storage/files/SDlrhr.png.meta
Normal 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
3
test_expiry.txt
Normal 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
111
test_storage.py
Normal 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
22
wsgi.py
Normal 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)
|
||||
Reference in New Issue
Block a user