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