Initial commit.
This commit is contained in:
commit
ce8012779b
183
.env.example
Normal file
183
.env.example
Normal file
@ -0,0 +1,183 @@
|
||||
# ============================================================================
|
||||
# WebDAV Server Configuration
|
||||
# Copy this file to .env and modify as needed
|
||||
# ============================================================================
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Server Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
HOST=0.0.0.0
|
||||
PORT=8080
|
||||
|
||||
# SSL/TLS Configuration (recommended for production)
|
||||
SSL_ENABLED=false
|
||||
SSL_CERT_PATH=/path/to/cert.pem
|
||||
SSL_KEY_PATH=/path/to/key.pem
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Database Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
DB_PATH=./webdav.db
|
||||
DB_BACKUP_INTERVAL=3600
|
||||
DB_VACUUM_INTERVAL=86400
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Authentication Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Supported methods: basic, digest, token (comma-separated)
|
||||
AUTH_METHODS=basic,digest
|
||||
|
||||
# Secret key for JWT and session encryption (generate with: python -c "import secrets; print(secrets.token_hex(32))")
|
||||
JWT_SECRET_KEY=your-secret-key-here-change-this-in-production
|
||||
|
||||
# Session timeout in seconds (1 hour default)
|
||||
SESSION_TIMEOUT=3600
|
||||
|
||||
# Password requirements
|
||||
PASSWORD_MIN_LENGTH=8
|
||||
PASSWORD_REQUIRE_UPPERCASE=true
|
||||
PASSWORD_REQUIRE_NUMBERS=true
|
||||
PASSWORD_REQUIRE_SPECIAL=true
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# WebDAV Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# WebDAV root directory (where user files are stored)
|
||||
WEBDAV_ROOT=./webdav
|
||||
|
||||
# Maximum file size in bytes (100MB default)
|
||||
MAX_FILE_SIZE=104857600
|
||||
|
||||
# Maximum PROPFIND depth (3 default, use lower value for better performance)
|
||||
MAX_PROPFIND_DEPTH=3
|
||||
|
||||
# Default lock timeout in seconds (1 hour default)
|
||||
LOCK_TIMEOUT_DEFAULT=3600
|
||||
|
||||
# Windows compatibility mode (recommended: true)
|
||||
ENABLE_WINDOWS_COMPATIBILITY=true
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Performance Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Maximum concurrent connections
|
||||
MAX_CONNECTIONS=1000
|
||||
|
||||
# Keep-alive timeout in seconds
|
||||
KEEPALIVE_TIMEOUT=30
|
||||
|
||||
# Client timeout in seconds
|
||||
CLIENT_TIMEOUT=60
|
||||
|
||||
# Worker connections (for production deployment)
|
||||
WORKER_CONNECTIONS=1024
|
||||
|
||||
# Number of worker processes (for gunicorn)
|
||||
WORKERS=4
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Logging Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
||||
LOG_LEVEL=INFO
|
||||
|
||||
# Log file paths
|
||||
LOG_FILE=./logs/webdav.log
|
||||
ACCESS_LOG_FILE=./logs/access.log
|
||||
ERROR_LOG_FILE=./logs/error.log
|
||||
|
||||
# Enable/disable specific logs
|
||||
ACCESS_LOG_ENABLED=true
|
||||
ERROR_LOG_ENABLED=true
|
||||
DEBUG_LOG_ENABLED=false
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Security Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# CORS settings
|
||||
CORS_ENABLED=false
|
||||
CORS_ORIGINS=*
|
||||
CORS_METHODS=GET,POST,PUT,DELETE,OPTIONS,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK
|
||||
|
||||
# Rate limiting
|
||||
RATE_LIMIT_ENABLED=true
|
||||
RATE_LIMIT_REQUESTS=100
|
||||
RATE_LIMIT_WINDOW=60
|
||||
|
||||
# Brute force protection
|
||||
MAX_LOGIN_ATTEMPTS=5
|
||||
LOGIN_ATTEMPT_WINDOW=300
|
||||
ACCOUNT_LOCKOUT_DURATION=900
|
||||
|
||||
# IP whitelist/blacklist (comma-separated)
|
||||
IP_WHITELIST=
|
||||
IP_BLACKLIST=
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Storage Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# User storage quotas in bytes (0 = unlimited)
|
||||
DEFAULT_USER_QUOTA=10737418240 # 10GB
|
||||
|
||||
# Shared folder configuration
|
||||
SHARED_FOLDER_ENABLED=true
|
||||
PUBLIC_FOLDER_ENABLED=false
|
||||
|
||||
# File versioning
|
||||
ENABLE_FILE_VERSIONING=false
|
||||
MAX_FILE_VERSIONS=5
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Cache Configuration (Optional - requires Redis)
|
||||
# ----------------------------------------------------------------------------
|
||||
CACHE_ENABLED=false
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_DB=0
|
||||
REDIS_PASSWORD=
|
||||
|
||||
# Cache TTL in seconds
|
||||
CACHE_TTL_SESSION=3600
|
||||
CACHE_TTL_LOCKS=1800
|
||||
CACHE_TTL_PROPERTIES=300
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Monitoring and Metrics (Optional)
|
||||
# ----------------------------------------------------------------------------
|
||||
METRICS_ENABLED=false
|
||||
PROMETHEUS_PORT=9090
|
||||
|
||||
# Health check endpoint
|
||||
HEALTHCHECK_ENABLED=true
|
||||
HEALTHCHECK_PATH=/health
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Email Configuration (for notifications)
|
||||
# ----------------------------------------------------------------------------
|
||||
EMAIL_ENABLED=false
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=your-email@gmail.com
|
||||
SMTP_PASSWORD=your-app-password
|
||||
SMTP_FROM=webdav@example.com
|
||||
SMTP_USE_TLS=true
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Backup Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
BACKUP_ENABLED=true
|
||||
BACKUP_PATH=./backups
|
||||
BACKUP_SCHEDULE=0 2 * * * # Daily at 2 AM (cron format)
|
||||
BACKUP_RETENTION_DAYS=30
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Development Settings
|
||||
# ----------------------------------------------------------------------------
|
||||
# Enable debug mode (DO NOT use in production)
|
||||
DEBUG=false
|
||||
|
||||
# Auto-reload on code changes (development only)
|
||||
AUTO_RELOAD=false
|
||||
|
||||
# Enable detailed error messages
|
||||
DETAILED_ERRORS=false
|
||||
78
Dockerfile
Normal file
78
Dockerfile
Normal file
@ -0,0 +1,78 @@
|
||||
# Multi-stage Dockerfile for WebDAV Server
|
||||
# Optimized for production deployment
|
||||
|
||||
# ============================================================================
|
||||
# Stage 1: Builder - Install dependencies and prepare environment
|
||||
# ============================================================================
|
||||
FROM python:3.11-slim as builder
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /build
|
||||
|
||||
# Install build dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gcc \
|
||||
g++ \
|
||||
libxml2-dev \
|
||||
libxslt-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy requirements
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install Python dependencies to a target directory
|
||||
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Stage 2: Runtime - Create minimal production image
|
||||
# ============================================================================
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Set labels for metadata
|
||||
LABEL maintainer="WebDAV Server <webdav@example.com>"
|
||||
LABEL description="Production-ready WebDAV Server with aiohttp"
|
||||
LABEL version="1.0.0"
|
||||
|
||||
# Create app user for security (don't run as root)
|
||||
RUN groupadd -r webdav && useradd -r -g webdav webdav
|
||||
|
||||
# Install runtime dependencies only
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libxml2 \
|
||||
libxslt1.1 \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy Python packages from builder
|
||||
COPY --from=builder /install /usr/local
|
||||
|
||||
# Copy application files
|
||||
COPY main.py .
|
||||
COPY config.py .
|
||||
COPY .env.example .env
|
||||
|
||||
# Create necessary directories
|
||||
RUN mkdir -p /app/webdav /app/logs /app/backups && \
|
||||
chown -R webdav:webdav /app
|
||||
|
||||
# Switch to non-root user
|
||||
USER webdav
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:8080/ || exit 1
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=8080
|
||||
|
||||
# Run application
|
||||
CMD ["python", "main.py"]
|
||||
450
QUICKSTART.md
Normal file
450
QUICKSTART.md
Normal file
@ -0,0 +1,450 @@
|
||||
# WebDAV Server - Quick Start Guide
|
||||
|
||||
Get your WebDAV server up and running in 5 minutes!
|
||||
|
||||
## 🚀 Instant Setup
|
||||
|
||||
### Option 1: Automated Installation (Recommended)
|
||||
|
||||
```bash
|
||||
# Download and run the setup script
|
||||
chmod +x setup.sh
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
The script will:
|
||||
- ✅ Check system requirements
|
||||
- ✅ Install dependencies
|
||||
- ✅ Create virtual environment
|
||||
- ✅ Set up configuration
|
||||
- ✅ Initialize database
|
||||
- ✅ Create default admin user
|
||||
|
||||
### Option 2: Manual Installation
|
||||
|
||||
```bash
|
||||
# 1. Create project directory
|
||||
mkdir webdav-server && cd webdav-server
|
||||
|
||||
# 2. Create virtual environment
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# 3. Install dependencies
|
||||
pip install aiohttp aiofiles aiohttp-session cryptography python-dotenv lxml gunicorn
|
||||
|
||||
# 4. Copy the main.py file from the artifacts
|
||||
|
||||
# 5. Create .env file
|
||||
cat > .env << 'EOF'
|
||||
HOST=0.0.0.0
|
||||
PORT=8080
|
||||
DB_PATH=./webdav.db
|
||||
WEBDAV_ROOT=./webdav
|
||||
AUTH_METHODS=basic,digest
|
||||
JWT_SECRET_KEY=$(python -c "import secrets; print(secrets.token_hex(32))")
|
||||
SESSION_TIMEOUT=3600
|
||||
EOF
|
||||
|
||||
# 6. Create directory structure
|
||||
mkdir -p webdav/users logs backups
|
||||
|
||||
# 7. Run the server
|
||||
python main.py
|
||||
```
|
||||
|
||||
### Option 3: Docker (Fastest)
|
||||
|
||||
```bash
|
||||
# 1. Create docker-compose.yml and Dockerfile from artifacts
|
||||
|
||||
# 2. Start with Docker Compose
|
||||
docker-compose up -d
|
||||
|
||||
# 3. Check logs
|
||||
docker-compose logs -f webdav
|
||||
```
|
||||
|
||||
## 🎯 First Steps
|
||||
|
||||
### 1. Access the Server
|
||||
|
||||
**Default Credentials:**
|
||||
- Username: `admin`
|
||||
- Password: `admin123`
|
||||
- URL: `http://localhost:8080/`
|
||||
|
||||
⚠️ **IMPORTANT**: Change the default password immediately!
|
||||
|
||||
### 2. Connect with Windows Explorer
|
||||
|
||||
**Method 1: Map Network Drive**
|
||||
1. Open File Explorer
|
||||
2. Right-click "This PC" → "Map network drive"
|
||||
3. Enter: `http://localhost:8080/`
|
||||
4. Enter credentials: `admin` / `admin123`
|
||||
|
||||
**Method 2: Add Network Location**
|
||||
1. Right-click "This PC" → "Add a network location"
|
||||
2. Choose custom network location
|
||||
3. Enter: `http://localhost:8080/`
|
||||
4. Enter credentials when prompted
|
||||
|
||||
**Windows Troubleshooting:**
|
||||
- If connection fails, restart WebClient service:
|
||||
```powershell
|
||||
net stop webclient
|
||||
net start webclient
|
||||
```
|
||||
|
||||
### 3. Connect with macOS Finder
|
||||
|
||||
1. Press `Cmd+K` (Go → Connect to Server)
|
||||
2. Enter: `http://localhost:8080/`
|
||||
3. Click "Connect"
|
||||
4. Enter credentials: `admin` / `admin123`
|
||||
|
||||
### 4. Connect with Linux
|
||||
|
||||
```bash
|
||||
# Install davfs2
|
||||
sudo apt-get install davfs2
|
||||
|
||||
# Mount WebDAV share
|
||||
sudo mount -t davfs http://localhost:8080/ /mnt/webdav
|
||||
|
||||
# Enter credentials when prompted
|
||||
```
|
||||
|
||||
## 👥 User Management
|
||||
|
||||
### Create Users via CLI
|
||||
|
||||
```bash
|
||||
# Activate virtual environment
|
||||
source venv/bin/activate
|
||||
|
||||
# Create a new user (interactive)
|
||||
python webdav_cli.py user create john
|
||||
|
||||
# Create with all details
|
||||
python webdav_cli.py user create jane \
|
||||
--email jane@example.com \
|
||||
--role user
|
||||
|
||||
# List all users
|
||||
python webdav_cli.py user list
|
||||
|
||||
# Change password
|
||||
python webdav_cli.py user password john
|
||||
|
||||
# Deactivate user
|
||||
python webdav_cli.py user deactivate john
|
||||
|
||||
# Delete user
|
||||
python webdav_cli.py user delete john --force
|
||||
```
|
||||
|
||||
### Create Users Programmatically
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from main import Database
|
||||
|
||||
async def create_user():
|
||||
db = Database('./webdav.db')
|
||||
user_id = await db.create_user(
|
||||
username='john',
|
||||
password='SecurePass123!',
|
||||
email='john@example.com',
|
||||
role='user'
|
||||
)
|
||||
print(f"Created user ID: {user_id}")
|
||||
|
||||
asyncio.run(create_user())
|
||||
```
|
||||
|
||||
## 🔧 Common Operations
|
||||
|
||||
### Upload Files
|
||||
|
||||
**Using curl:**
|
||||
```bash
|
||||
curl -u admin:admin123 -T myfile.txt http://localhost:8080/myfile.txt
|
||||
```
|
||||
|
||||
**Using Python:**
|
||||
```python
|
||||
import requests
|
||||
|
||||
with open('myfile.txt', 'rb') as f:
|
||||
response = requests.put(
|
||||
'http://localhost:8080/myfile.txt',
|
||||
data=f,
|
||||
auth=('admin', 'admin123')
|
||||
)
|
||||
print(response.status_code)
|
||||
```
|
||||
|
||||
### Download Files
|
||||
|
||||
```bash
|
||||
curl -u admin:admin123 http://localhost:8080/myfile.txt -o downloaded.txt
|
||||
```
|
||||
|
||||
### Create Directories
|
||||
|
||||
```bash
|
||||
curl -u admin:admin123 -X MKCOL http://localhost:8080/newfolder/
|
||||
```
|
||||
|
||||
### List Directory Contents
|
||||
|
||||
```bash
|
||||
curl -u admin:admin123 -X PROPFIND http://localhost:8080/ \
|
||||
-H "Depth: 1" \
|
||||
-H "Content-Type: application/xml" \
|
||||
--data '<?xml version="1.0"?><D:propfind xmlns:D="DAV:"><D:allprop/></D:propfind>'
|
||||
```
|
||||
|
||||
### Copy Files
|
||||
|
||||
```bash
|
||||
curl -u admin:admin123 -X COPY \
|
||||
http://localhost:8080/source.txt \
|
||||
-H "Destination: http://localhost:8080/destination.txt"
|
||||
```
|
||||
|
||||
### Move Files
|
||||
|
||||
```bash
|
||||
curl -u admin:admin123 -X MOVE \
|
||||
http://localhost:8080/old.txt \
|
||||
-H "Destination: http://localhost:8080/new.txt"
|
||||
```
|
||||
|
||||
### Delete Files
|
||||
|
||||
```bash
|
||||
curl -u admin:admin123 -X DELETE http://localhost:8080/myfile.txt
|
||||
```
|
||||
|
||||
## 🔒 Security Setup
|
||||
|
||||
### Change Default Password
|
||||
|
||||
```bash
|
||||
python webdav_cli.py user password admin
|
||||
# Enter new password when prompted
|
||||
```
|
||||
|
||||
### Enable HTTPS (Production)
|
||||
|
||||
1. **Get SSL Certificate (Let's Encrypt):**
|
||||
```bash
|
||||
sudo certbot certonly --standalone -d webdav.example.com
|
||||
```
|
||||
|
||||
2. **Update .env:**
|
||||
```env
|
||||
SSL_ENABLED=true
|
||||
SSL_CERT_PATH=/etc/letsencrypt/live/webdav.example.com/fullchain.pem
|
||||
SSL_KEY_PATH=/etc/letsencrypt/live/webdav.example.com/privkey.pem
|
||||
```
|
||||
|
||||
3. **Restart server**
|
||||
|
||||
### Use Nginx Reverse Proxy (Recommended)
|
||||
|
||||
1. Install Nginx:
|
||||
```bash
|
||||
sudo apt-get install nginx
|
||||
```
|
||||
|
||||
2. Copy nginx.conf from artifacts to `/etc/nginx/conf.d/webdav.conf`
|
||||
|
||||
3. Update domain name in config
|
||||
|
||||
4. Reload Nginx:
|
||||
```bash
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## 📊 Monitoring
|
||||
|
||||
### View Statistics
|
||||
|
||||
```bash
|
||||
python webdav_cli.py stats
|
||||
```
|
||||
|
||||
### Check Active Locks
|
||||
|
||||
```bash
|
||||
python webdav_cli.py lock list
|
||||
```
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
# Application logs
|
||||
tail -f logs/webdav.log
|
||||
|
||||
# Access logs
|
||||
tail -f logs/access.log
|
||||
|
||||
# If using systemd
|
||||
sudo journalctl -u webdav -f
|
||||
```
|
||||
|
||||
## 🐳 Docker Deployment
|
||||
|
||||
### Start Service
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
docker-compose logs -f
|
||||
```
|
||||
|
||||
### Stop Service
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Update and Restart
|
||||
|
||||
```bash
|
||||
docker-compose pull
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
### Backup Data
|
||||
|
||||
```bash
|
||||
# Backup database
|
||||
docker exec webdav-server python webdav_cli.py backup
|
||||
|
||||
# Backup files
|
||||
docker cp webdav-server:/app/webdav ./backup-webdav/
|
||||
```
|
||||
|
||||
## ⚙️ Production Deployment
|
||||
|
||||
### Using Gunicorn (Recommended)
|
||||
|
||||
```bash
|
||||
# Start with Gunicorn
|
||||
gunicorn main:init_app \
|
||||
--config gunicorn_config.py \
|
||||
--bind 0.0.0.0:8080 \
|
||||
--workers 4
|
||||
|
||||
# With systemd (after setup)
|
||||
sudo systemctl enable webdav
|
||||
sudo systemctl start webdav
|
||||
sudo systemctl status webdav
|
||||
```
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
**Development:**
|
||||
```env
|
||||
DEBUG=true
|
||||
LOG_LEVEL=DEBUG
|
||||
WORKERS=1
|
||||
```
|
||||
|
||||
**Production:**
|
||||
```env
|
||||
DEBUG=false
|
||||
LOG_LEVEL=INFO
|
||||
WORKERS=4
|
||||
SSL_ENABLED=true
|
||||
RATE_LIMIT_ENABLED=true
|
||||
```
|
||||
|
||||
## 🔥 Common Issues
|
||||
|
||||
### Issue: Connection Refused
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Check if server is running
|
||||
ps aux | grep python
|
||||
|
||||
# Check port
|
||||
sudo netstat -tlnp | grep 8080
|
||||
|
||||
# Check firewall
|
||||
sudo ufw allow 8080/tcp
|
||||
```
|
||||
|
||||
### Issue: Permission Denied
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Fix directory permissions
|
||||
chmod -R 755 webdav/
|
||||
chown -R $USER:$USER webdav/
|
||||
```
|
||||
|
||||
### Issue: Database Locked
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Stop all instances
|
||||
pkill -f "python main.py"
|
||||
|
||||
# Check for locks
|
||||
lsof webdav.db
|
||||
|
||||
# Restart server
|
||||
python main.py
|
||||
```
|
||||
|
||||
### Issue: Windows Explorer Won't Connect
|
||||
|
||||
**Solutions:**
|
||||
1. Restart WebClient service
|
||||
2. Enable Basic Auth in registry:
|
||||
```
|
||||
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters
|
||||
BasicAuthLevel = 2
|
||||
```
|
||||
3. Use HTTPS instead of HTTP
|
||||
4. Ensure URL ends with `/`
|
||||
|
||||
## 📚 Next Steps
|
||||
|
||||
1. **Read the full README.md** for comprehensive documentation
|
||||
2. **Configure SSL/TLS** for production use
|
||||
3. **Set up automated backups**
|
||||
4. **Configure monitoring** and alerts
|
||||
5. **Review security settings**
|
||||
6. **Set up user quotas** and permissions
|
||||
7. **Configure rate limiting**
|
||||
8. **Enable caching** with Redis
|
||||
|
||||
## 🆘 Getting Help
|
||||
|
||||
- **Documentation**: Check README.md and inline comments
|
||||
- **Logs**: Always check logs first (`logs/webdav.log`)
|
||||
- **CLI Help**: `python webdav_cli.py --help`
|
||||
- **Test Connection**: `curl -v http://localhost:8080/`
|
||||
|
||||
## 🎉 You're All Set!
|
||||
|
||||
Your WebDAV server is now ready to use. Start by:
|
||||
1. Changing the default admin password
|
||||
2. Creating user accounts
|
||||
3. Connecting with your favorite WebDAV client
|
||||
4. Uploading some files
|
||||
|
||||
Happy file sharing! 🚀
|
||||
641
README.md
Normal file
641
README.md
Normal file
@ -0,0 +1,641 @@
|
||||
# WebDAV Server with aiohttp
|
||||
|
||||
A comprehensive, production-ready WebDAV server implementation with full RFC 4918 compliance, Windows Explorer compatibility, and enterprise-grade features.
|
||||
|
||||
## Features
|
||||
|
||||
### Core WebDAV Protocol
|
||||
- ✅ **Full RFC 4918 Compliance**: All standard WebDAV methods implemented
|
||||
- ✅ **Windows Explorer Integration**: Seamless compatibility with Windows WebDAV Mini-Redirector
|
||||
- ✅ **Multiple Authentication Methods**: Basic, Digest, and Token-based authentication
|
||||
- ✅ **Resource Locking**: Exclusive and shared locks with timeout management
|
||||
- ✅ **Custom Properties**: Full property management (PROPFIND/PROPPATCH)
|
||||
- ✅ **Collection Operations**: MKCOL, COPY, MOVE with depth support
|
||||
|
||||
### Enterprise Features
|
||||
- 🔒 **Multi-User Support**: Isolated user directories with permissions
|
||||
- 💾 **SQLite Database**: Robust backend for users, locks, and properties
|
||||
- 🌐 **Web Management Interface**: Admin dashboard and user portal
|
||||
- 🚀 **High Performance**: Async I/O with aiohttp and aiofiles
|
||||
- 🔐 **Security Focused**: Input validation, path traversal prevention, SQL injection protection
|
||||
- 📊 **Monitoring Ready**: Structured logging and metrics collection
|
||||
- 🐳 **Docker Support**: Container-ready with docker-compose
|
||||
- 📈 **Production Ready**: Gunicorn integration, health checks, graceful shutdown
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.8 or higher
|
||||
- pip (Python package manager)
|
||||
- Optional: Redis for caching
|
||||
- Optional: Docker for containerized deployment
|
||||
|
||||
### Installation
|
||||
|
||||
1. **Clone or create the project directory:**
|
||||
```bash
|
||||
mkdir webdav-server
|
||||
cd webdav-server
|
||||
```
|
||||
|
||||
2. **Install dependencies:**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **Configure environment:**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# Edit .env with your configuration
|
||||
nano .env
|
||||
```
|
||||
|
||||
4. **Generate a secure secret key:**
|
||||
```bash
|
||||
python -c "import secrets; print(secrets.token_hex(32))"
|
||||
# Copy the output to JWT_SECRET_KEY in .env
|
||||
```
|
||||
|
||||
5. **Run the server:**
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
The server will start on `http://0.0.0.0:8080` by default.
|
||||
|
||||
### First Run
|
||||
|
||||
On first run, the server automatically creates:
|
||||
- A default admin user: `admin` / `admin123`
|
||||
- The WebDAV root directory structure
|
||||
- SQLite database with all required tables
|
||||
|
||||
**⚠️ Important**: Change the default admin password immediately!
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
All configuration is done through the `.env` file. Key settings:
|
||||
|
||||
#### Server Configuration
|
||||
```env
|
||||
HOST=0.0.0.0 # Listen address
|
||||
PORT=8080 # Listen port
|
||||
SSL_ENABLED=false # Enable HTTPS
|
||||
```
|
||||
|
||||
#### Authentication
|
||||
```env
|
||||
AUTH_METHODS=basic,digest # Supported auth methods
|
||||
JWT_SECRET_KEY=... # Secret for sessions (required)
|
||||
SESSION_TIMEOUT=3600 # Session duration in seconds
|
||||
```
|
||||
|
||||
#### WebDAV Settings
|
||||
```env
|
||||
WEBDAV_ROOT=./webdav # Root directory for files
|
||||
MAX_FILE_SIZE=104857600 # Max file size (100MB)
|
||||
MAX_PROPFIND_DEPTH=3 # Depth limit for PROPFIND
|
||||
LOCK_TIMEOUT_DEFAULT=3600 # Default lock timeout
|
||||
```
|
||||
|
||||
See `.env.example` for complete configuration options.
|
||||
|
||||
## Usage
|
||||
|
||||
### Connecting with Windows Explorer
|
||||
|
||||
#### Method 1: Map Network Drive
|
||||
|
||||
1. Open File Explorer
|
||||
2. Right-click "This PC" → "Map network drive"
|
||||
3. Choose a drive letter
|
||||
4. Enter the WebDAV URL:
|
||||
```
|
||||
http://your-server:8080/
|
||||
```
|
||||
5. Check "Connect using different credentials"
|
||||
6. Enter your username and password
|
||||
7. Click "Finish"
|
||||
|
||||
#### Method 2: Add Network Location
|
||||
|
||||
1. Open File Explorer
|
||||
2. Right-click "This PC" → "Add a network location"
|
||||
3. Choose "Choose a custom network location"
|
||||
4. Enter the WebDAV URL:
|
||||
```
|
||||
http://your-server:8080/
|
||||
```
|
||||
5. Enter credentials when prompted
|
||||
|
||||
#### Windows with SSL (Recommended)
|
||||
|
||||
For HTTPS connections:
|
||||
```
|
||||
https://your-server:8443/
|
||||
```
|
||||
|
||||
**Note**: Windows requires port 443 for HTTPS WebDAV by default. To use other ports, modify Windows registry:
|
||||
```
|
||||
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters
|
||||
BasicAuthLevel = 2
|
||||
```
|
||||
|
||||
### Connecting with macOS Finder
|
||||
|
||||
1. Open Finder
|
||||
2. Press `Cmd+K` (Go → Connect to Server)
|
||||
3. Enter the WebDAV URL:
|
||||
```
|
||||
http://your-server:8080/
|
||||
```
|
||||
4. Click "Connect"
|
||||
5. Enter your credentials
|
||||
|
||||
### Connecting with Linux (davfs2)
|
||||
|
||||
1. Install davfs2:
|
||||
```bash
|
||||
sudo apt-get install davfs2 # Ubuntu/Debian
|
||||
```
|
||||
|
||||
2. Mount the WebDAV share:
|
||||
```bash
|
||||
sudo mount -t davfs http://your-server:8080/ /mnt/webdav
|
||||
```
|
||||
|
||||
3. Enter credentials when prompted
|
||||
|
||||
### Command Line Tools
|
||||
|
||||
#### Using curl
|
||||
|
||||
**Upload a file:**
|
||||
```bash
|
||||
curl -u username:password -T file.txt http://localhost:8080/file.txt
|
||||
```
|
||||
|
||||
**Download a file:**
|
||||
```bash
|
||||
curl -u username:password http://localhost:8080/file.txt -o file.txt
|
||||
```
|
||||
|
||||
**Create a directory:**
|
||||
```bash
|
||||
curl -u username:password -X MKCOL http://localhost:8080/newdir/
|
||||
```
|
||||
|
||||
**Delete a resource:**
|
||||
```bash
|
||||
curl -u username:password -X DELETE http://localhost:8080/file.txt
|
||||
```
|
||||
|
||||
#### Using cadaver
|
||||
|
||||
```bash
|
||||
cadaver http://localhost:8080/
|
||||
# Enter credentials
|
||||
dav:/> ls
|
||||
dav:/> put localfile.txt
|
||||
dav:/> get remotefile.txt
|
||||
dav:/> mkcol newfolder
|
||||
```
|
||||
|
||||
## User Management
|
||||
|
||||
### Creating Users Programmatically
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from main import Database
|
||||
|
||||
async def create_user():
|
||||
db = Database('./webdav.db')
|
||||
user_id = await db.create_user(
|
||||
username='john',
|
||||
password='SecurePass123!',
|
||||
email='john@example.com',
|
||||
role='user'
|
||||
)
|
||||
print(f"Created user with ID: {user_id}")
|
||||
|
||||
asyncio.run(create_user())
|
||||
```
|
||||
|
||||
### User Roles
|
||||
|
||||
- **admin**: Full access to all features and user management
|
||||
- **user**: Standard user with access to their own directory
|
||||
- **readonly**: Read-only access (future implementation)
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
webdav/
|
||||
├── users/
|
||||
│ ├── admin/ # Admin user directory
|
||||
│ ├── john/ # John's private directory
|
||||
│ └── jane/ # Jane's private directory
|
||||
├── shared/ # Shared between all users (optional)
|
||||
└── public/ # Public access (optional)
|
||||
```
|
||||
|
||||
## Production Deployment
|
||||
|
||||
### Using Gunicorn
|
||||
|
||||
Create `gunicorn_config.py`:
|
||||
```python
|
||||
import multiprocessing
|
||||
|
||||
bind = "0.0.0.0:8080"
|
||||
workers = multiprocessing.cpu_count() * 2 + 1
|
||||
worker_class = "aiohttp.GunicornWebWorker"
|
||||
keepalive = 30
|
||||
timeout = 60
|
||||
accesslog = "./logs/access.log"
|
||||
errorlog = "./logs/error.log"
|
||||
loglevel = "info"
|
||||
```
|
||||
|
||||
Run with Gunicorn:
|
||||
```bash
|
||||
gunicorn main:init_app --config gunicorn_config.py
|
||||
```
|
||||
|
||||
### Using Docker
|
||||
|
||||
Create `Dockerfile`:
|
||||
```dockerfile
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy application
|
||||
COPY . .
|
||||
|
||||
# Create directories
|
||||
RUN mkdir -p /app/webdav /app/logs
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Run application
|
||||
CMD ["python", "main.py"]
|
||||
```
|
||||
|
||||
Create `docker-compose.yml`:
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
webdav:
|
||||
build: .
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./webdav:/app/webdav
|
||||
- ./logs:/app/logs
|
||||
- ./webdav.db:/app/webdav.db
|
||||
environment:
|
||||
- HOST=0.0.0.0
|
||||
- PORT=8080
|
||||
- DB_PATH=/app/webdav.db
|
||||
- WEBDAV_ROOT=/app/webdav
|
||||
restart: unless-stopped
|
||||
|
||||
# Optional: Redis for caching
|
||||
redis:
|
||||
image: redis:alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
restart: unless-stopped
|
||||
```
|
||||
|
||||
Build and run:
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Nginx Reverse Proxy
|
||||
|
||||
Create `/etc/nginx/sites-available/webdav`:
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name webdav.example.com;
|
||||
|
||||
# Redirect to HTTPS
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name webdav.example.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/webdav.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/webdav.example.com/privkey.pem;
|
||||
|
||||
# WebDAV specific settings
|
||||
client_max_body_size 100M;
|
||||
client_body_buffer_size 128k;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8080;
|
||||
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;
|
||||
|
||||
# WebDAV headers
|
||||
proxy_set_header Destination $http_destination;
|
||||
proxy_set_header Depth $http_depth;
|
||||
|
||||
# Timeouts
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Enable and reload:
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/webdav /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### SSL/TLS Configuration
|
||||
|
||||
1. **Always use HTTPS in production**
|
||||
2. Generate SSL certificates with Let's Encrypt:
|
||||
```bash
|
||||
sudo certbot certonly --nginx -d webdav.example.com
|
||||
```
|
||||
3. Update `.env`:
|
||||
```env
|
||||
SSL_ENABLED=true
|
||||
SSL_CERT_PATH=/etc/letsencrypt/live/webdav.example.com/fullchain.pem
|
||||
SSL_KEY_PATH=/etc/letsencrypt/live/webdav.example.com/privkey.pem
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
- Use **Digest authentication** over Basic when possible
|
||||
- Enforce strong passwords (min length, complexity)
|
||||
- Enable rate limiting to prevent brute force attacks
|
||||
- Implement account lockout after failed attempts
|
||||
|
||||
### Firewall Rules
|
||||
|
||||
```bash
|
||||
# Allow only necessary ports
|
||||
sudo ufw allow 80/tcp
|
||||
sudo ufw allow 443/tcp
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
### Regular Updates
|
||||
|
||||
```bash
|
||||
# Update dependencies
|
||||
pip install --upgrade -r requirements.txt
|
||||
|
||||
# Backup database before updates
|
||||
cp webdav.db webdav.db.backup
|
||||
```
|
||||
|
||||
## Monitoring and Logging
|
||||
|
||||
### Log Files
|
||||
|
||||
Logs are stored in `./logs/` directory:
|
||||
- `webdav.log` - Application logs
|
||||
- `access.log` - Access logs (requests)
|
||||
- `error.log` - Error logs
|
||||
|
||||
### Log Format
|
||||
|
||||
JSON-structured logs for easy parsing:
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-01-15T10:30:45Z",
|
||||
"level": "INFO",
|
||||
"user": "john",
|
||||
"method": "PROPFIND",
|
||||
"path": "/documents/",
|
||||
"status": 207,
|
||||
"duration_ms": 45
|
||||
}
|
||||
```
|
||||
|
||||
### Health Check
|
||||
|
||||
Access the health check endpoint:
|
||||
```bash
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Windows Explorer Connection Issues
|
||||
|
||||
**Problem**: "The network folder is no longer available"
|
||||
|
||||
**Solutions**:
|
||||
1. Increase Windows WebClient timeout:
|
||||
```
|
||||
Registry: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters
|
||||
FileSizeLimitInBytes = DWORD: 0xFFFFFFFF
|
||||
```
|
||||
|
||||
2. Enable Basic Authentication over HTTP (insecure - use only for testing):
|
||||
```
|
||||
Registry: HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\WebClient\Parameters
|
||||
BasicAuthLevel = 2
|
||||
```
|
||||
|
||||
3. Restart WebClient service:
|
||||
```powershell
|
||||
net stop webclient
|
||||
net start webclient
|
||||
```
|
||||
|
||||
**Problem**: "The specified server cannot perform the requested operation"
|
||||
|
||||
**Solution**: Ensure the URL ends with a slash and doesn't include port 80:
|
||||
- ❌ `http://server:80/path`
|
||||
- ✅ `http://server/path/`
|
||||
|
||||
### macOS Finder Issues
|
||||
|
||||
**Problem**: Connection refused or timeout
|
||||
|
||||
**Solutions**:
|
||||
1. Use HTTP URL with explicit protocol:
|
||||
```
|
||||
http://server:8080/
|
||||
```
|
||||
|
||||
2. Check firewall settings on server
|
||||
|
||||
3. Try connecting via IP address instead of hostname
|
||||
|
||||
### Performance Issues
|
||||
|
||||
**Problem**: Slow PROPFIND responses
|
||||
|
||||
**Solutions**:
|
||||
1. Reduce `MAX_PROPFIND_DEPTH` in `.env`:
|
||||
```env
|
||||
MAX_PROPFIND_DEPTH=1
|
||||
```
|
||||
|
||||
2. Enable caching with Redis:
|
||||
```env
|
||||
CACHE_ENABLED=true
|
||||
REDIS_HOST=localhost
|
||||
```
|
||||
|
||||
3. Increase worker processes:
|
||||
```env
|
||||
WORKERS=8
|
||||
```
|
||||
|
||||
### Database Locked Errors
|
||||
|
||||
**Problem**: "database is locked" errors
|
||||
|
||||
**Solutions**:
|
||||
1. Enable WAL mode (automatic in code)
|
||||
2. Ensure only one process accesses the database
|
||||
3. Use separate databases for multiple instances
|
||||
|
||||
## API Documentation
|
||||
|
||||
### WebDAV Methods
|
||||
|
||||
#### PROPFIND - Property Discovery
|
||||
|
||||
```http
|
||||
PROPFIND /documents/ HTTP/1.1
|
||||
Depth: 1
|
||||
Content-Type: application/xml
|
||||
|
||||
<?xml version="1.0"?>
|
||||
<D:propfind xmlns:D="DAV:">
|
||||
<D:allprop/>
|
||||
</D:propfind>
|
||||
```
|
||||
|
||||
#### PROPPATCH - Property Modification
|
||||
|
||||
```http
|
||||
PROPPATCH /file.txt HTTP/1.1
|
||||
Content-Type: application/xml
|
||||
|
||||
<?xml version="1.0"?>
|
||||
<D:propertyupdate xmlns:D="DAV:">
|
||||
<D:set>
|
||||
<D:prop>
|
||||
<D:displayname>My Document</D:displayname>
|
||||
</D:prop>
|
||||
</D:set>
|
||||
</D:propertyupdate>
|
||||
```
|
||||
|
||||
#### LOCK - Resource Locking
|
||||
|
||||
```http
|
||||
LOCK /file.txt HTTP/1.1
|
||||
Timeout: Second-3600
|
||||
Content-Type: application/xml
|
||||
|
||||
<?xml version="1.0"?>
|
||||
<D:lockinfo xmlns:D="DAV:">
|
||||
<D:lockscope><D:exclusive/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
<D:owner>
|
||||
<D:href>mailto:john@example.com</D:href>
|
||||
</D:owner>
|
||||
</D:lockinfo>
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Install test dependencies
|
||||
pip install pytest pytest-asyncio pytest-aiohttp pytest-cov
|
||||
|
||||
# Run tests
|
||||
pytest tests/ -v
|
||||
|
||||
# Run with coverage
|
||||
pytest tests/ --cov=. --cov-report=html
|
||||
```
|
||||
|
||||
### Code Style
|
||||
|
||||
```bash
|
||||
# Format code
|
||||
black main.py
|
||||
|
||||
# Lint code
|
||||
flake8 main.py
|
||||
|
||||
# Type checking
|
||||
mypy main.py
|
||||
```
|
||||
|
||||
### Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests
|
||||
5. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
MIT License - See LICENSE file for details
|
||||
|
||||
## Support
|
||||
|
||||
For issues, questions, or contributions:
|
||||
- GitHub Issues: [Create an issue]
|
||||
- Documentation: [Wiki]
|
||||
- Community: [Discussions]
|
||||
|
||||
## Changelog
|
||||
|
||||
### Version 1.0.0 (2025-01-15)
|
||||
- Initial release
|
||||
- Full RFC 4918 compliance
|
||||
- Windows Explorer compatibility
|
||||
- Multi-user support with SQLite backend
|
||||
- Basic and Digest authentication
|
||||
- Resource locking
|
||||
- Custom properties
|
||||
- Production-ready with Gunicorn support
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- RFC 4918 - HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)
|
||||
- aiohttp - Async HTTP client/server framework
|
||||
- Python community
|
||||
|
||||
---
|
||||
|
||||
**Made with ❤️ for the WebDAV community**
|
||||
145
docker-compose.yml
Normal file
145
docker-compose.yml
Normal file
@ -0,0 +1,145 @@
|
||||
version: '3.8'
|
||||
|
||||
# ============================================================================
|
||||
# WebDAV Server - Docker Compose Configuration
|
||||
# ============================================================================
|
||||
|
||||
services:
|
||||
# --------------------------------------------------------------------------
|
||||
# WebDAV Server
|
||||
# --------------------------------------------------------------------------
|
||||
webdav:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: webdav-server
|
||||
restart: unless-stopped
|
||||
|
||||
ports:
|
||||
- "8080:8080"
|
||||
|
||||
volumes:
|
||||
# Persistent storage for user files
|
||||
- ./webdav:/app/webdav
|
||||
|
||||
# Database persistence
|
||||
- ./webdav.db:/app/webdav.db
|
||||
|
||||
# Logs
|
||||
- ./logs:/app/logs
|
||||
|
||||
# Backups
|
||||
- ./backups:/app/backups
|
||||
|
||||
# Configuration (optional - uncomment to override)
|
||||
# - ./.env:/app/.env:ro
|
||||
|
||||
environment:
|
||||
# Server Configuration
|
||||
- HOST=0.0.0.0
|
||||
- PORT=8080
|
||||
|
||||
# Database
|
||||
- DB_PATH=/app/webdav.db
|
||||
|
||||
# WebDAV Root
|
||||
- WEBDAV_ROOT=/app/webdav
|
||||
|
||||
# Authentication
|
||||
- AUTH_METHODS=basic,digest
|
||||
- SESSION_TIMEOUT=3600
|
||||
|
||||
# Performance
|
||||
- MAX_CONNECTIONS=1000
|
||||
- WORKERS=4
|
||||
|
||||
# Logging
|
||||
- LOG_LEVEL=INFO
|
||||
- LOG_FILE=/app/logs/webdav.log
|
||||
|
||||
# Cache (if Redis enabled)
|
||||
- CACHE_ENABLED=false
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
|
||||
networks:
|
||||
- webdav-network
|
||||
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8080/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Redis Cache (Optional but recommended for production)
|
||||
# --------------------------------------------------------------------------
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: webdav-redis
|
||||
restart: unless-stopped
|
||||
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
|
||||
|
||||
networks:
|
||||
- webdav-network
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Nginx Reverse Proxy with SSL (Production)
|
||||
# --------------------------------------------------------------------------
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: webdav-nginx
|
||||
restart: unless-stopped
|
||||
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
|
||||
volumes:
|
||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
||||
- ./logs/nginx:/var/log/nginx
|
||||
|
||||
networks:
|
||||
- webdav-network
|
||||
|
||||
depends_on:
|
||||
- webdav
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# ============================================================================
|
||||
# Networks
|
||||
# ============================================================================
|
||||
networks:
|
||||
webdav-network:
|
||||
driver: bridge
|
||||
|
||||
# ============================================================================
|
||||
# Volumes
|
||||
# ============================================================================
|
||||
volumes:
|
||||
redis-data:
|
||||
driver: local
|
||||
223
gunicorn_config.py
Normal file
223
gunicorn_config.py
Normal file
@ -0,0 +1,223 @@
|
||||
"""
|
||||
Gunicorn Configuration for WebDAV Server
|
||||
Production-grade WSGI server configuration with aiohttp worker
|
||||
"""
|
||||
|
||||
import os
|
||||
import multiprocessing
|
||||
|
||||
# ============================================================================
|
||||
# Server Socket
|
||||
# ============================================================================
|
||||
|
||||
bind = f"{os.getenv('HOST', '0.0.0.0')}:{os.getenv('PORT', '8080')}"
|
||||
backlog = 2048
|
||||
|
||||
# ============================================================================
|
||||
# Worker Processes
|
||||
# ============================================================================
|
||||
|
||||
# Number of worker processes
|
||||
workers = int(os.getenv('WORKERS', multiprocessing.cpu_count() * 2 + 1))
|
||||
|
||||
# Worker class - MUST be aiohttp.GunicornWebWorker for aiohttp
|
||||
worker_class = 'aiohttp.GunicornWebWorker'
|
||||
|
||||
# Worker connections (only for async workers)
|
||||
worker_connections = int(os.getenv('WORKER_CONNECTIONS', 1024))
|
||||
|
||||
# Maximum requests a worker will process before restarting (prevents memory leaks)
|
||||
max_requests = int(os.getenv('MAX_REQUESTS', 10000))
|
||||
max_requests_jitter = int(os.getenv('MAX_REQUESTS_JITTER', 1000))
|
||||
|
||||
# Worker timeout in seconds
|
||||
timeout = int(os.getenv('WORKER_TIMEOUT', 60))
|
||||
|
||||
# Graceful timeout for workers
|
||||
graceful_timeout = int(os.getenv('GRACEFUL_TIMEOUT', 30))
|
||||
|
||||
# Keep-alive timeout
|
||||
keepalive = int(os.getenv('KEEPALIVE_TIMEOUT', 30))
|
||||
|
||||
# ============================================================================
|
||||
# Logging
|
||||
# ============================================================================
|
||||
|
||||
# Access log
|
||||
accesslog = os.getenv('ACCESS_LOG_FILE', './logs/gunicorn_access.log')
|
||||
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
|
||||
|
||||
# Error log
|
||||
errorlog = os.getenv('ERROR_LOG_FILE', './logs/gunicorn_error.log')
|
||||
|
||||
# Log level
|
||||
loglevel = os.getenv('LOG_LEVEL', 'info').lower()
|
||||
|
||||
# Capture output from workers
|
||||
capture_output = True
|
||||
|
||||
# Enable log rotation
|
||||
logconfig_dict = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'generic': {
|
||||
'format': '%(asctime)s [%(process)d] [%(levelname)s] %(message)s',
|
||||
'datefmt': '%Y-%m-%d %H:%M:%S',
|
||||
'class': 'logging.Formatter'
|
||||
},
|
||||
'access': {
|
||||
'format': '%(message)s',
|
||||
'class': 'logging.Formatter'
|
||||
}
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'generic',
|
||||
'stream': 'ext://sys.stdout'
|
||||
},
|
||||
'error_file': {
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'formatter': 'generic',
|
||||
'filename': errorlog,
|
||||
'maxBytes': 10485760, # 10MB
|
||||
'backupCount': 5
|
||||
},
|
||||
'access_file': {
|
||||
'class': 'logging.handlers.RotatingFileHandler',
|
||||
'formatter': 'access',
|
||||
'filename': accesslog,
|
||||
'maxBytes': 10485760, # 10MB
|
||||
'backupCount': 5
|
||||
}
|
||||
},
|
||||
'loggers': {
|
||||
'gunicorn.error': {
|
||||
'handlers': ['console', 'error_file'],
|
||||
'level': loglevel.upper(),
|
||||
'propagate': False
|
||||
},
|
||||
'gunicorn.access': {
|
||||
'handlers': ['access_file'],
|
||||
'level': 'INFO',
|
||||
'propagate': False
|
||||
}
|
||||
},
|
||||
'root': {
|
||||
'level': loglevel.upper(),
|
||||
'handlers': ['console', 'error_file']
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Process Naming
|
||||
# ============================================================================
|
||||
|
||||
proc_name = 'webdav_server'
|
||||
|
||||
# ============================================================================
|
||||
# Server Mechanics
|
||||
# ============================================================================
|
||||
|
||||
# Daemon mode (run in background)
|
||||
daemon = os.getenv('DAEMON', 'false').lower() == 'true'
|
||||
|
||||
# PID file
|
||||
pidfile = os.getenv('PID_FILE', './gunicorn.pid')
|
||||
|
||||
# User and group to run workers
|
||||
user = os.getenv('USER', None)
|
||||
group = os.getenv('GROUP', None)
|
||||
|
||||
# Directory to switch to before loading apps
|
||||
chdir = os.getenv('CHDIR', '.')
|
||||
|
||||
# Environment variables to set for workers
|
||||
raw_env = [
|
||||
f"WEBDAV_ENV={os.getenv('WEBDAV_ENV', 'production')}"
|
||||
]
|
||||
|
||||
# ============================================================================
|
||||
# Server Hooks
|
||||
# ============================================================================
|
||||
|
||||
def on_starting(server):
|
||||
"""Called just before the master process is initialized."""
|
||||
server.log.info("Starting WebDAV Server...")
|
||||
|
||||
# Create necessary directories
|
||||
os.makedirs('./logs', exist_ok=True)
|
||||
os.makedirs('./webdav', exist_ok=True)
|
||||
os.makedirs('./backups', exist_ok=True)
|
||||
|
||||
|
||||
def on_reload(server):
|
||||
"""Called to recycle workers during a reload."""
|
||||
server.log.info("Reloading WebDAV Server...")
|
||||
|
||||
|
||||
def when_ready(server):
|
||||
"""Called just after the server is started."""
|
||||
server.log.info(f"WebDAV Server is ready. Listening on: {bind}")
|
||||
server.log.info(f"Workers: {workers}")
|
||||
server.log.info(f"Worker class: {worker_class}")
|
||||
|
||||
|
||||
def pre_fork(server, worker):
|
||||
"""Called just before a worker is forked."""
|
||||
pass
|
||||
|
||||
|
||||
def post_fork(server, worker):
|
||||
"""Called just after a worker has been forked."""
|
||||
server.log.info(f"Worker spawned (pid: {worker.pid})")
|
||||
|
||||
|
||||
def pre_exec(server):
|
||||
"""Called just before a new master process is forked."""
|
||||
server.log.info("Forked child, re-executing.")
|
||||
|
||||
|
||||
def worker_int(worker):
|
||||
"""Called when a worker receives the SIGINT or SIGQUIT signal."""
|
||||
worker.log.info(f"Worker received INT or QUIT signal (pid: {worker.pid})")
|
||||
|
||||
|
||||
def worker_abort(worker):
|
||||
"""Called when a worker receives the SIGABRT signal."""
|
||||
worker.log.info(f"Worker received SIGABRT signal (pid: {worker.pid})")
|
||||
|
||||
|
||||
def pre_request(worker, req):
|
||||
"""Called just before a worker processes the request."""
|
||||
worker.log.debug(f"{req.method} {req.path}")
|
||||
|
||||
|
||||
def post_request(worker, req, environ, resp):
|
||||
"""Called after a worker processes the request."""
|
||||
pass
|
||||
|
||||
|
||||
def worker_exit(server, worker):
|
||||
"""Called just after a worker has been exited."""
|
||||
server.log.info(f"Worker exited (pid: {worker.pid})")
|
||||
|
||||
|
||||
def on_exit(server):
|
||||
"""Called just before exiting Gunicorn."""
|
||||
server.log.info("Shutting down WebDAV Server...")
|
||||
|
||||
# ============================================================================
|
||||
# SSL Configuration (if needed)
|
||||
# ============================================================================
|
||||
|
||||
# Uncomment and configure for SSL support
|
||||
# keyfile = '/path/to/key.pem'
|
||||
# certfile = '/path/to/cert.pem'
|
||||
# ssl_version = 'TLSv1_2'
|
||||
# cert_reqs = 0 # ssl.CERT_NONE
|
||||
# ca_certs = None
|
||||
# suppress_ragged_eofs = True
|
||||
# do_handshake_on_connect = False
|
||||
# ciphers = 'TLSv1'
|
||||
153
nginx.conf
Normal file
153
nginx.conf
Normal file
@ -0,0 +1,153 @@
|
||||
# ============================================================================
|
||||
# Nginx Configuration for WebDAV Server
|
||||
# Place this in: /etc/nginx/conf.d/webdav.conf
|
||||
# ============================================================================
|
||||
|
||||
# Upstream backend
|
||||
upstream webdav_backend {
|
||||
server webdav:8080 max_fails=3 fail_timeout=30s;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
# HTTP Server - Redirect to HTTPS
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name webdav.example.com; # Change to your domain
|
||||
|
||||
# Let's Encrypt ACME challenge
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/certbot;
|
||||
}
|
||||
|
||||
# Redirect all other traffic to HTTPS
|
||||
location / {
|
||||
return 301 https://$server_name$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS Server - Main WebDAV
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name webdav.example.com; # Change to your domain
|
||||
|
||||
# SSL Configuration
|
||||
ssl_certificate /etc/nginx/ssl/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
|
||||
|
||||
# SSL Security Settings
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
# Security Headers
|
||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
|
||||
# WebDAV Specific Settings
|
||||
client_max_body_size 1G; # Maximum file size
|
||||
client_body_buffer_size 128k;
|
||||
client_body_timeout 300s; # Timeout for client body
|
||||
client_header_timeout 60s;
|
||||
send_timeout 300s; # Timeout for sending response
|
||||
|
||||
# Proxy buffering (disable for large files)
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
|
||||
# Timeouts
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
|
||||
# Logging
|
||||
access_log /var/log/nginx/webdav_access.log combined;
|
||||
error_log /var/log/nginx/webdav_error.log warn;
|
||||
|
||||
# Root location - WebDAV
|
||||
location / {
|
||||
# Proxy to backend
|
||||
proxy_pass http://webdav_backend;
|
||||
|
||||
# Standard proxy headers
|
||||
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;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
|
||||
# WebDAV specific headers
|
||||
proxy_set_header Destination $http_destination;
|
||||
proxy_set_header Depth $http_depth;
|
||||
proxy_set_header Overwrite $http_overwrite;
|
||||
proxy_set_header Lock-Token $http_lock_token;
|
||||
proxy_set_header Timeout $http_timeout;
|
||||
proxy_set_header If $http_if;
|
||||
|
||||
# HTTP 1.1 support
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
|
||||
# Disable redirects
|
||||
proxy_redirect off;
|
||||
|
||||
# Handle errors
|
||||
proxy_intercept_errors off;
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
proxy_pass http://webdav_backend/health;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# Deny access to hidden files
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# HTTP Configuration without SSL (for development only)
|
||||
# ============================================================================
|
||||
|
||||
# Uncomment this section for development without SSL
|
||||
# server {
|
||||
# listen 80;
|
||||
# listen [::]:80;
|
||||
# server_name webdav.example.com;
|
||||
#
|
||||
# client_max_body_size 1G;
|
||||
# client_body_buffer_size 128k;
|
||||
# client_body_timeout 300s;
|
||||
# send_timeout 300s;
|
||||
#
|
||||
# location / {
|
||||
# proxy_pass http://webdav_backend;
|
||||
# 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;
|
||||
#
|
||||
# # WebDAV headers
|
||||
# proxy_set_header Destination $http_destination;
|
||||
# proxy_set_header Depth $http_depth;
|
||||
# proxy_set_header Overwrite $http_overwrite;
|
||||
# proxy_set_header Lock-Token $http_lock_token;
|
||||
#
|
||||
# proxy_http_version 1.1;
|
||||
# proxy_set_header Connection "";
|
||||
# proxy_redirect off;
|
||||
# proxy_buffering off;
|
||||
# }
|
||||
# }
|
||||
40
requirements.txt
Normal file
40
requirements.txt
Normal file
@ -0,0 +1,40 @@
|
||||
# Core Web Framework
|
||||
aiohttp>=3.9.0
|
||||
aiofiles>=23.2.0
|
||||
aiohttp-session>=2.12.0
|
||||
aiohttp-security>=0.4.0
|
||||
|
||||
# Security and Cryptography
|
||||
cryptography>=41.0.0
|
||||
PyJWT>=2.8.0
|
||||
|
||||
# Environment and Configuration
|
||||
python-dotenv>=1.0.0
|
||||
|
||||
# XML Processing
|
||||
lxml>=4.9.0
|
||||
|
||||
# Template Engine (for web interface)
|
||||
Jinja2>=3.1.2
|
||||
MarkupSafe>=2.1.3
|
||||
|
||||
# Production Server
|
||||
gunicorn>=21.2.0
|
||||
|
||||
# Testing
|
||||
pytest>=7.4.0
|
||||
pytest-asyncio>=0.21.0
|
||||
pytest-aiohttp>=1.0.5
|
||||
pytest-cov>=4.1.0
|
||||
|
||||
# Development Tools
|
||||
black>=23.7.0
|
||||
flake8>=6.1.0
|
||||
mypy>=1.5.0
|
||||
|
||||
# Optional: Redis for caching
|
||||
redis>=5.0.0
|
||||
aioredis>=2.0.1
|
||||
|
||||
# Optional: Performance monitoring
|
||||
prometheus-client>=0.17.1
|
||||
362
setup.sh
Executable file
362
setup.sh
Executable file
@ -0,0 +1,362 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# WebDAV Server Installation and Setup Script
|
||||
# ============================================================================
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Functions
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ ${1}${NC}"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓ ${1}${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠ ${1}${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗ ${1}${NC}"
|
||||
}
|
||||
|
||||
print_header() {
|
||||
echo ""
|
||||
echo -e "${BLUE}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${BLUE}║ WebDAV Server Installation Script ║${NC}"
|
||||
echo -e "${BLUE}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Check if running as root
|
||||
check_root() {
|
||||
if [ "$EUID" -eq 0 ]; then
|
||||
print_warning "Running as root. It's recommended to run as a regular user."
|
||||
read -p "Continue? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Check system requirements
|
||||
check_requirements() {
|
||||
print_info "Checking system requirements..."
|
||||
|
||||
# Check Python version
|
||||
if command -v python3 &> /dev/null; then
|
||||
PYTHON_VERSION=$(python3 --version | awk '{print $2}')
|
||||
print_success "Python $PYTHON_VERSION found"
|
||||
else
|
||||
print_error "Python 3 not found. Please install Python 3.8 or higher."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check pip
|
||||
if command -v pip3 &> /dev/null; then
|
||||
print_success "pip3 found"
|
||||
else
|
||||
print_warning "pip3 not found. Installing..."
|
||||
python3 -m ensurepip --upgrade
|
||||
fi
|
||||
|
||||
# Check git (optional)
|
||||
if command -v git &> /dev/null; then
|
||||
print_success "git found"
|
||||
else
|
||||
print_warning "git not found. Some features may be limited."
|
||||
fi
|
||||
}
|
||||
|
||||
# Install system dependencies
|
||||
install_dependencies() {
|
||||
print_info "Installing system dependencies..."
|
||||
|
||||
if [ -f /etc/debian_version ]; then
|
||||
# Debian/Ubuntu
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3-dev python3-venv build-essential libxml2-dev libxslt-dev
|
||||
print_success "Dependencies installed (Debian/Ubuntu)"
|
||||
elif [ -f /etc/redhat-release ]; then
|
||||
# RHEL/CentOS/Fedora
|
||||
sudo yum install -y python3-devel gcc gcc-c++ libxml2-devel libxslt-devel
|
||||
print_success "Dependencies installed (RHEL/CentOS/Fedora)"
|
||||
else
|
||||
print_warning "Unknown distribution. Please install python3-dev, build-essential manually."
|
||||
fi
|
||||
}
|
||||
|
||||
# Setup installation directory
|
||||
setup_directory() {
|
||||
print_info "Setting up installation directory..."
|
||||
|
||||
read -p "Installation directory [./webdav-server]: " INSTALL_DIR
|
||||
INSTALL_DIR=${INSTALL_DIR:-./webdav-server}
|
||||
|
||||
if [ -d "$INSTALL_DIR" ]; then
|
||||
print_warning "Directory $INSTALL_DIR already exists."
|
||||
read -p "Continue and potentially overwrite files? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
mkdir -p "$INSTALL_DIR"
|
||||
fi
|
||||
|
||||
cd "$INSTALL_DIR"
|
||||
print_success "Using directory: $(pwd)"
|
||||
}
|
||||
|
||||
# Create virtual environment
|
||||
setup_venv() {
|
||||
print_info "Creating Python virtual environment..."
|
||||
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
pip install --upgrade pip setuptools wheel
|
||||
print_success "Virtual environment created"
|
||||
}
|
||||
|
||||
# Install Python packages
|
||||
install_packages() {
|
||||
print_info "Installing Python packages..."
|
||||
|
||||
source venv/bin/activate
|
||||
|
||||
if [ -f requirements.txt ]; then
|
||||
pip install -r requirements.txt
|
||||
print_success "Packages installed from requirements.txt"
|
||||
else
|
||||
print_warning "requirements.txt not found. Installing core packages..."
|
||||
pip install aiohttp aiofiles aiohttp-session cryptography python-dotenv lxml gunicorn
|
||||
print_success "Core packages installed"
|
||||
fi
|
||||
}
|
||||
|
||||
# Setup configuration
|
||||
setup_config() {
|
||||
print_info "Setting up configuration..."
|
||||
|
||||
if [ ! -f .env ]; then
|
||||
if [ -f .env.example ]; then
|
||||
cp .env.example .env
|
||||
print_success "Created .env from .env.example"
|
||||
else
|
||||
print_warning ".env.example not found. Creating basic .env..."
|
||||
cat > .env << EOF
|
||||
HOST=0.0.0.0
|
||||
PORT=8080
|
||||
DB_PATH=./webdav.db
|
||||
WEBDAV_ROOT=./webdav
|
||||
AUTH_METHODS=basic,digest
|
||||
JWT_SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
|
||||
SESSION_TIMEOUT=3600
|
||||
MAX_FILE_SIZE=104857600
|
||||
LOG_LEVEL=INFO
|
||||
EOF
|
||||
print_success "Created basic .env file"
|
||||
fi
|
||||
else
|
||||
print_warning ".env already exists. Skipping..."
|
||||
fi
|
||||
|
||||
# Generate secret key if needed
|
||||
if grep -q "your-secret-key-here" .env 2>/dev/null; then
|
||||
SECRET_KEY=$(python3 -c "import secrets; print(secrets.token_hex(32))")
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
# macOS
|
||||
sed -i '' "s/your-secret-key-here-change-this-in-production/$SECRET_KEY/" .env
|
||||
else
|
||||
# Linux
|
||||
sed -i "s/your-secret-key-here-change-this-in-production/$SECRET_KEY/" .env
|
||||
fi
|
||||
print_success "Generated new JWT secret key"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create directory structure
|
||||
create_directories() {
|
||||
print_info "Creating directory structure..."
|
||||
|
||||
mkdir -p webdav/users
|
||||
mkdir -p webdav/shared
|
||||
mkdir -p logs
|
||||
mkdir -p backups
|
||||
|
||||
print_success "Directory structure created"
|
||||
}
|
||||
|
||||
# Initialize database
|
||||
init_database() {
|
||||
print_info "Initializing database..."
|
||||
|
||||
source venv/bin/activate
|
||||
python3 << EOF
|
||||
import asyncio
|
||||
from main import Database, create_default_user
|
||||
|
||||
async def init():
|
||||
db = Database('./webdav.db')
|
||||
await create_default_user(db)
|
||||
|
||||
asyncio.run(init())
|
||||
EOF
|
||||
|
||||
print_success "Database initialized"
|
||||
}
|
||||
|
||||
# Setup systemd service (optional)
|
||||
setup_systemd() {
|
||||
print_info "Would you like to set up systemd service? (requires sudo)"
|
||||
read -p "Setup systemd service? (y/n) " -n 1 -r
|
||||
echo
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
# Create webdav user if doesn't exist
|
||||
if ! id -u webdav &>/dev/null; then
|
||||
sudo useradd -r -s /bin/false webdav
|
||||
print_success "Created webdav user"
|
||||
fi
|
||||
|
||||
# Copy service file
|
||||
if [ -f webdav.service ]; then
|
||||
sudo cp webdav.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
print_success "Systemd service installed"
|
||||
|
||||
print_info "To enable and start the service:"
|
||||
echo " sudo systemctl enable webdav"
|
||||
echo " sudo systemctl start webdav"
|
||||
echo " sudo systemctl status webdav"
|
||||
else
|
||||
print_warning "webdav.service not found. Skipping..."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Setup nginx (optional)
|
||||
setup_nginx() {
|
||||
print_info "Would you like to set up Nginx reverse proxy?"
|
||||
read -p "Setup Nginx? (y/n) " -n 1 -r
|
||||
echo
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
if command -v nginx &> /dev/null; then
|
||||
read -p "Enter your domain name: " DOMAIN_NAME
|
||||
|
||||
if [ -f nginx.conf ]; then
|
||||
mkdir -p nginx/conf.d nginx/ssl
|
||||
sed "s/webdav.example.com/$DOMAIN_NAME/g" nginx.conf > nginx/conf.d/webdav.conf
|
||||
|
||||
print_success "Nginx configuration created"
|
||||
print_info "Configuration saved to: nginx/conf.d/webdav.conf"
|
||||
print_warning "Don't forget to configure SSL certificates!"
|
||||
else
|
||||
print_warning "nginx.conf template not found"
|
||||
fi
|
||||
else
|
||||
print_warning "Nginx not installed. Skipping..."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Setup Docker (optional)
|
||||
setup_docker() {
|
||||
print_info "Would you like to set up Docker deployment?"
|
||||
read -p "Setup Docker? (y/n) " -n 1 -r
|
||||
echo
|
||||
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
if command -v docker &> /dev/null; then
|
||||
if [ -f Dockerfile ] && [ -f docker-compose.yml ]; then
|
||||
print_info "Building Docker image..."
|
||||
docker-compose build
|
||||
print_success "Docker image built"
|
||||
|
||||
print_info "To start the service:"
|
||||
echo " docker-compose up -d"
|
||||
echo " docker-compose logs -f"
|
||||
else
|
||||
print_warning "Dockerfile or docker-compose.yml not found"
|
||||
fi
|
||||
else
|
||||
print_warning "Docker not installed. Skipping..."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Final instructions
|
||||
print_instructions() {
|
||||
echo ""
|
||||
echo -e "${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║ Installation Complete! ║${NC}"
|
||||
echo -e "${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
|
||||
print_info "Getting Started:"
|
||||
echo ""
|
||||
echo "1. Activate virtual environment:"
|
||||
echo " source venv/bin/activate"
|
||||
echo ""
|
||||
echo "2. Start the server:"
|
||||
echo " python main.py"
|
||||
echo " OR with Gunicorn (production):"
|
||||
echo " gunicorn main:init_app --config gunicorn_config.py"
|
||||
echo ""
|
||||
echo "3. Access WebDAV server:"
|
||||
echo " http://localhost:8080/"
|
||||
echo ""
|
||||
echo "4. Default credentials:"
|
||||
echo " Username: admin"
|
||||
echo " Password: admin123"
|
||||
echo " ⚠️ CHANGE THIS PASSWORD IMMEDIATELY!"
|
||||
echo ""
|
||||
echo "5. Manage users with CLI:"
|
||||
echo " python webdav_cli.py user create newuser"
|
||||
echo " python webdav_cli.py user list"
|
||||
echo " python webdav_cli.py stats"
|
||||
echo ""
|
||||
print_warning "Remember to:"
|
||||
echo " • Change the default admin password"
|
||||
echo " • Configure SSL/TLS for production"
|
||||
echo " • Set up firewall rules"
|
||||
echo " • Configure backups"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Main installation process
|
||||
main() {
|
||||
print_header
|
||||
|
||||
check_root
|
||||
check_requirements
|
||||
install_dependencies
|
||||
setup_directory
|
||||
setup_venv
|
||||
install_packages
|
||||
setup_config
|
||||
create_directories
|
||||
init_database
|
||||
setup_systemd
|
||||
setup_nginx
|
||||
setup_docker
|
||||
print_instructions
|
||||
|
||||
print_success "Installation complete!"
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main
|
||||
565
test_webdav.py
Normal file
565
test_webdav.py
Normal file
@ -0,0 +1,565 @@
|
||||
"""
|
||||
Comprehensive Test Suite for WebDAV Server
|
||||
Tests all WebDAV methods, authentication, and edge cases
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import asyncio
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from aiohttp import web
|
||||
from aiohttp.test_utils import TestClient, TestServer
|
||||
import base64
|
||||
|
||||
# Import the main application
|
||||
import sys
|
||||
sys.path.insert(0, '.')
|
||||
from main import Database, AuthHandler, WebDAVHandler, init_app, Config
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Fixtures
|
||||
# ============================================================================
|
||||
|
||||
@pytest.fixture
|
||||
async def temp_dir():
|
||||
"""Create temporary directory for tests"""
|
||||
temp_path = Path(tempfile.mkdtemp())
|
||||
yield temp_path
|
||||
shutil.rmtree(temp_path, ignore_errors=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def test_db(temp_dir):
|
||||
"""Create test database"""
|
||||
db_path = temp_dir / 'test.db'
|
||||
db = Database(str(db_path))
|
||||
|
||||
# Create test users
|
||||
await db.create_user('testuser', 'testpass123', 'test@example.com', 'user')
|
||||
await db.create_user('admin', 'adminpass123', 'admin@example.com', 'admin')
|
||||
|
||||
yield db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def test_app(test_db, temp_dir, monkeypatch):
|
||||
"""Create test application"""
|
||||
# Override config for testing
|
||||
monkeypatch.setattr(Config, 'DB_PATH', str(temp_dir / 'test.db'))
|
||||
monkeypatch.setattr(Config, 'WEBDAV_ROOT', str(temp_dir / 'webdav'))
|
||||
|
||||
# Create webdav root
|
||||
(temp_dir / 'webdav' / 'users' / 'testuser').mkdir(parents=True, exist_ok=True)
|
||||
(temp_dir / 'webdav' / 'users' / 'admin').mkdir(parents=True, exist_ok=True)
|
||||
|
||||
app = await init_app()
|
||||
app['db'] = test_db
|
||||
|
||||
yield app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def client(test_app):
|
||||
"""Create test client"""
|
||||
server = TestServer(test_app)
|
||||
client = TestClient(server)
|
||||
|
||||
await client.start_server()
|
||||
yield client
|
||||
await client.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def basic_auth_header():
|
||||
"""Create basic auth header"""
|
||||
credentials = base64.b64encode(b'testuser:testpass123').decode('utf-8')
|
||||
return {'Authorization': f'Basic {credentials}'}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Authentication Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestAuthentication:
|
||||
"""Test authentication mechanisms"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_auth_returns_401(self, client):
|
||||
"""Test that requests without auth return 401"""
|
||||
response = await client.get('/')
|
||||
assert response.status == 401
|
||||
assert 'WWW-Authenticate' in response.headers
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_basic_auth_success(self, client, basic_auth_header):
|
||||
"""Test successful basic authentication"""
|
||||
response = await client.get('/', headers=basic_auth_header)
|
||||
assert response.status in [200, 207] # 207 for PROPFIND
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_basic_auth_invalid_credentials(self, client):
|
||||
"""Test basic auth with invalid credentials"""
|
||||
credentials = base64.b64encode(b'testuser:wrongpass').decode('utf-8')
|
||||
headers = {'Authorization': f'Basic {credentials}'}
|
||||
response = await client.get('/', headers=headers)
|
||||
assert response.status == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_basic_auth_malformed(self, client):
|
||||
"""Test basic auth with malformed header"""
|
||||
headers = {'Authorization': 'Basic invalid-base64'}
|
||||
response = await client.get('/', headers=headers)
|
||||
assert response.status == 401
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# HTTP Methods Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestHTTPMethods:
|
||||
"""Test standard HTTP methods"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_options(self, client, basic_auth_header):
|
||||
"""Test OPTIONS method"""
|
||||
response = await client.options('/', headers=basic_auth_header)
|
||||
assert response.status == 200
|
||||
assert 'DAV' in response.headers
|
||||
assert 'Allow' in response.headers
|
||||
|
||||
allow = response.headers['Allow']
|
||||
assert 'PROPFIND' in allow
|
||||
assert 'PROPPATCH' in allow
|
||||
assert 'MKCOL' in allow
|
||||
assert 'LOCK' in allow
|
||||
assert 'UNLOCK' in allow
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_nonexistent_file(self, client, basic_auth_header):
|
||||
"""Test GET on nonexistent file"""
|
||||
response = await client.get('/nonexistent.txt', headers=basic_auth_header)
|
||||
assert response.status == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_put_and_get_file(self, client, basic_auth_header):
|
||||
"""Test PUT and GET file"""
|
||||
content = b'Hello, WebDAV!'
|
||||
|
||||
# PUT file
|
||||
put_response = await client.put(
|
||||
'/test.txt',
|
||||
data=content,
|
||||
headers=basic_auth_header
|
||||
)
|
||||
assert put_response.status in [201, 204]
|
||||
|
||||
# GET file
|
||||
get_response = await client.get('/test.txt', headers=basic_auth_header)
|
||||
assert get_response.status == 200
|
||||
|
||||
body = await get_response.read()
|
||||
assert body == content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_head_file(self, client, basic_auth_header):
|
||||
"""Test HEAD method"""
|
||||
# Create a file first
|
||||
content = b'Test content'
|
||||
await client.put('/test.txt', data=content, headers=basic_auth_header)
|
||||
|
||||
# HEAD request
|
||||
response = await client.head('/test.txt', headers=basic_auth_header)
|
||||
assert response.status == 200
|
||||
assert 'Content-Length' in response.headers
|
||||
assert int(response.headers['Content-Length']) == len(content)
|
||||
|
||||
# Body should be empty
|
||||
body = await response.read()
|
||||
assert body == b''
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_file(self, client, basic_auth_header):
|
||||
"""Test DELETE method"""
|
||||
# Create a file
|
||||
await client.put('/test.txt', data=b'content', headers=basic_auth_header)
|
||||
|
||||
# Delete it
|
||||
response = await client.delete('/test.txt', headers=basic_auth_header)
|
||||
assert response.status == 204
|
||||
|
||||
# Verify it's gone
|
||||
get_response = await client.get('/test.txt', headers=basic_auth_header)
|
||||
assert get_response.status == 404
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# WebDAV Methods Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestWebDAVMethods:
|
||||
"""Test WebDAV-specific methods"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mkcol(self, client, basic_auth_header):
|
||||
"""Test MKCOL (create collection)"""
|
||||
response = await client.request(
|
||||
'MKCOL',
|
||||
'/newdir/',
|
||||
headers=basic_auth_header
|
||||
)
|
||||
assert response.status == 201
|
||||
|
||||
# Verify directory was created with PROPFIND
|
||||
propfind_response = await client.request(
|
||||
'PROPFIND',
|
||||
'/newdir/',
|
||||
headers={**basic_auth_header, 'Depth': '0'}
|
||||
)
|
||||
assert propfind_response.status == 207
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mkcol_already_exists(self, client, basic_auth_header):
|
||||
"""Test MKCOL on existing directory"""
|
||||
# Create directory
|
||||
await client.request('MKCOL', '/testdir/', headers=basic_auth_header)
|
||||
|
||||
# Try to create again
|
||||
response = await client.request('MKCOL', '/testdir/', headers=basic_auth_header)
|
||||
assert response.status == 405 # Method Not Allowed
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_propfind_depth_0(self, client, basic_auth_header):
|
||||
"""Test PROPFIND with depth 0"""
|
||||
# Create a file
|
||||
await client.put('/test.txt', data=b'content', headers=basic_auth_header)
|
||||
|
||||
# PROPFIND with depth 0
|
||||
propfind_body = b'''<?xml version="1.0"?>
|
||||
<D:propfind xmlns:D="DAV:">
|
||||
<D:allprop/>
|
||||
</D:propfind>'''
|
||||
|
||||
response = await client.request(
|
||||
'PROPFIND',
|
||||
'/test.txt',
|
||||
data=propfind_body,
|
||||
headers={**basic_auth_header, 'Depth': '0', 'Content-Type': 'application/xml'}
|
||||
)
|
||||
|
||||
assert response.status == 207
|
||||
body = await response.text()
|
||||
assert 'multistatus' in body
|
||||
assert 'test.txt' in body
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_propfind_depth_1(self, client, basic_auth_header):
|
||||
"""Test PROPFIND with depth 1"""
|
||||
# Create directory with files
|
||||
await client.request('MKCOL', '/testdir/', headers=basic_auth_header)
|
||||
await client.put('/testdir/file1.txt', data=b'content1', headers=basic_auth_header)
|
||||
await client.put('/testdir/file2.txt', data=b'content2', headers=basic_auth_header)
|
||||
|
||||
# PROPFIND with depth 1
|
||||
response = await client.request(
|
||||
'PROPFIND',
|
||||
'/testdir/',
|
||||
headers={**basic_auth_header, 'Depth': '1'}
|
||||
)
|
||||
|
||||
assert response.status == 207
|
||||
body = await response.text()
|
||||
assert 'file1.txt' in body
|
||||
assert 'file2.txt' in body
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proppatch(self, client, basic_auth_header):
|
||||
"""Test PROPPATCH (set properties)"""
|
||||
# Create a file
|
||||
await client.put('/test.txt', data=b'content', headers=basic_auth_header)
|
||||
|
||||
# Set custom property
|
||||
proppatch_body = b'''<?xml version="1.0"?>
|
||||
<D:propertyupdate xmlns:D="DAV:">
|
||||
<D:set>
|
||||
<D:prop>
|
||||
<D:displayname>My Document</D:displayname>
|
||||
</D:prop>
|
||||
</D:set>
|
||||
</D:propertyupdate>'''
|
||||
|
||||
response = await client.request(
|
||||
'PROPPATCH',
|
||||
'/test.txt',
|
||||
data=proppatch_body,
|
||||
headers={**basic_auth_header, 'Content-Type': 'application/xml'}
|
||||
)
|
||||
|
||||
assert response.status == 207
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_copy(self, client, basic_auth_header):
|
||||
"""Test COPY method"""
|
||||
# Create source file
|
||||
await client.put('/source.txt', data=b'content', headers=basic_auth_header)
|
||||
|
||||
# Copy to destination
|
||||
response = await client.request(
|
||||
'COPY',
|
||||
'/source.txt',
|
||||
headers={
|
||||
**basic_auth_header,
|
||||
'Destination': '/dest.txt'
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status in [201, 204]
|
||||
|
||||
# Verify both files exist
|
||||
source_response = await client.get('/source.txt', headers=basic_auth_header)
|
||||
assert source_response.status == 200
|
||||
|
||||
dest_response = await client.get('/dest.txt', headers=basic_auth_header)
|
||||
assert dest_response.status == 200
|
||||
|
||||
# Verify content is the same
|
||||
source_content = await source_response.read()
|
||||
dest_content = await dest_response.read()
|
||||
assert source_content == dest_content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_move(self, client, basic_auth_header):
|
||||
"""Test MOVE method"""
|
||||
# Create source file
|
||||
await client.put('/source.txt', data=b'content', headers=basic_auth_header)
|
||||
|
||||
# Move to destination
|
||||
response = await client.request(
|
||||
'MOVE',
|
||||
'/source.txt',
|
||||
headers={
|
||||
**basic_auth_header,
|
||||
'Destination': '/dest.txt'
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status in [201, 204]
|
||||
|
||||
# Verify source is gone
|
||||
source_response = await client.get('/source.txt', headers=basic_auth_header)
|
||||
assert source_response.status == 404
|
||||
|
||||
# Verify destination exists
|
||||
dest_response = await client.get('/dest.txt', headers=basic_auth_header)
|
||||
assert dest_response.status == 200
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lock_and_unlock(self, client, basic_auth_header):
|
||||
"""Test LOCK and UNLOCK methods"""
|
||||
# Create a file
|
||||
await client.put('/test.txt', data=b'content', headers=basic_auth_header)
|
||||
|
||||
# Lock the file
|
||||
lock_body = b'''<?xml version="1.0"?>
|
||||
<D:lockinfo xmlns:D="DAV:">
|
||||
<D:lockscope><D:exclusive/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
<D:owner>
|
||||
<D:href>mailto:test@example.com</D:href>
|
||||
</D:owner>
|
||||
</D:lockinfo>'''
|
||||
|
||||
lock_response = await client.request(
|
||||
'LOCK',
|
||||
'/test.txt',
|
||||
data=lock_body,
|
||||
headers={**basic_auth_header, 'Content-Type': 'application/xml', 'Timeout': 'Second-3600'}
|
||||
)
|
||||
|
||||
assert lock_response.status == 200
|
||||
assert 'Lock-Token' in lock_response.headers
|
||||
|
||||
lock_token = lock_response.headers['Lock-Token'].strip('<>')
|
||||
|
||||
# Unlock the file
|
||||
unlock_response = await client.request(
|
||||
'UNLOCK',
|
||||
'/test.txt',
|
||||
headers={**basic_auth_header, 'Lock-Token': f'<{lock_token}>'}
|
||||
)
|
||||
|
||||
assert unlock_response.status == 204
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Security Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestSecurity:
|
||||
"""Test security features"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_path_traversal_prevention(self, client, basic_auth_header):
|
||||
"""Test that path traversal is prevented"""
|
||||
# Try to access parent directory
|
||||
response = await client.get('/../../../etc/passwd', headers=basic_auth_header)
|
||||
assert response.status in [403, 404]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_user_isolation(self, client):
|
||||
"""Test that users can't access other users' files"""
|
||||
# Create file as testuser
|
||||
testuser_auth = base64.b64encode(b'testuser:testpass123').decode('utf-8')
|
||||
testuser_headers = {'Authorization': f'Basic {testuser_auth}'}
|
||||
|
||||
await client.put('/myfile.txt', data=b'secret', headers=testuser_headers)
|
||||
|
||||
# Try to access as admin
|
||||
admin_auth = base64.b64encode(b'admin:adminpass123').decode('utf-8')
|
||||
admin_headers = {'Authorization': f'Basic {admin_auth}'}
|
||||
|
||||
# This should fail because each user has their own isolated directory
|
||||
# The path /myfile.txt for admin is different from /myfile.txt for testuser
|
||||
response = await client.get('/myfile.txt', headers=admin_headers)
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Database Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestDatabase:
|
||||
"""Test database operations"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_user(self, test_db):
|
||||
"""Test user creation"""
|
||||
user_id = await test_db.create_user('newuser', 'password123', 'new@example.com')
|
||||
assert user_id > 0
|
||||
|
||||
# Verify user can be retrieved
|
||||
user = await test_db.verify_user('newuser', 'password123')
|
||||
assert user is not None
|
||||
assert user['username'] == 'newuser'
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_verify_user_wrong_password(self, test_db):
|
||||
"""Test user verification with wrong password"""
|
||||
user = await test_db.verify_user('testuser', 'wrongpassword')
|
||||
assert user is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lock_creation(self, test_db):
|
||||
"""Test lock creation"""
|
||||
lock_token = await test_db.create_lock(
|
||||
'/test.txt',
|
||||
1,
|
||||
'write',
|
||||
'exclusive',
|
||||
0,
|
||||
3600,
|
||||
'test@example.com'
|
||||
)
|
||||
|
||||
assert lock_token.startswith('opaquelocktoken:')
|
||||
|
||||
# Retrieve lock
|
||||
lock = await test_db.get_lock('/test.txt')
|
||||
assert lock is not None
|
||||
assert lock['lock_token'] == lock_token
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_lock_removal(self, test_db):
|
||||
"""Test lock removal"""
|
||||
lock_token = await test_db.create_lock('/test.txt', 1)
|
||||
|
||||
removed = await test_db.remove_lock(lock_token, 1)
|
||||
assert removed is True
|
||||
|
||||
# Verify lock is gone
|
||||
lock = await test_db.get_lock('/test.txt')
|
||||
assert lock is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_property_management(self, test_db):
|
||||
"""Test property set and get"""
|
||||
await test_db.set_property('/test.txt', 'DAV:', 'displayname', 'My File')
|
||||
|
||||
props = await test_db.get_properties('/test.txt')
|
||||
assert len(props) > 0
|
||||
assert any(p['property_name'] == 'displayname' for p in props)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Integration Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestIntegration:
|
||||
"""Integration tests for complex workflows"""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_complete_workflow(self, client, basic_auth_header):
|
||||
"""Test complete file management workflow"""
|
||||
# 1. Create directory
|
||||
await client.request('MKCOL', '/docs/', headers=basic_auth_header)
|
||||
|
||||
# 2. Upload files
|
||||
await client.put('/docs/file1.txt', data=b'content1', headers=basic_auth_header)
|
||||
await client.put('/docs/file2.txt', data=b'content2', headers=basic_auth_header)
|
||||
|
||||
# 3. List directory
|
||||
response = await client.request(
|
||||
'PROPFIND',
|
||||
'/docs/',
|
||||
headers={**basic_auth_header, 'Depth': '1'}
|
||||
)
|
||||
assert response.status == 207
|
||||
body = await response.text()
|
||||
assert 'file1.txt' in body
|
||||
assert 'file2.txt' in body
|
||||
|
||||
# 4. Copy file
|
||||
await client.request(
|
||||
'COPY',
|
||||
'/docs/file1.txt',
|
||||
headers={**basic_auth_header, 'Destination': '/docs/file1_copy.txt'}
|
||||
)
|
||||
|
||||
# 5. Move file
|
||||
await client.request(
|
||||
'MOVE',
|
||||
'/docs/file2.txt',
|
||||
headers={**basic_auth_header, 'Destination': '/docs/renamed.txt'}
|
||||
)
|
||||
|
||||
# 6. Verify final state
|
||||
response = await client.request(
|
||||
'PROPFIND',
|
||||
'/docs/',
|
||||
headers={**basic_auth_header, 'Depth': '1'}
|
||||
)
|
||||
body = await response.text()
|
||||
assert 'file1.txt' in body
|
||||
assert 'file1_copy.txt' in body
|
||||
assert 'renamed.txt' in body
|
||||
assert 'file2.txt' not in body
|
||||
|
||||
# 7. Delete directory
|
||||
await client.delete('/docs/', headers=basic_auth_header)
|
||||
|
||||
# 8. Verify deletion
|
||||
response = await client.request(
|
||||
'PROPFIND',
|
||||
'/docs/',
|
||||
headers={**basic_auth_header, 'Depth': '0'}
|
||||
)
|
||||
assert response.status == 404
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Run Tests
|
||||
# ============================================================================
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__, '-v', '--tb=short'])
|
||||
85
webdav.service
Normal file
85
webdav.service
Normal file
@ -0,0 +1,85 @@
|
||||
# ============================================================================
|
||||
# WebDAV Server Systemd Service
|
||||
# Installation: sudo cp webdav.service /etc/systemd/system/
|
||||
# Enable: sudo systemctl enable webdav
|
||||
# Start: sudo systemctl start webdav
|
||||
# Status: sudo systemctl status webdav
|
||||
# Logs: sudo journalctl -u webdav -f
|
||||
# ============================================================================
|
||||
|
||||
[Unit]
|
||||
Description=WebDAV Server with aiohttp
|
||||
Documentation=https://github.com/yourusername/webdav-server
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
|
||||
# User and group to run the service (create with: sudo useradd -r -s /bin/false webdav)
|
||||
User=webdav
|
||||
Group=webdav
|
||||
|
||||
# Working directory
|
||||
WorkingDirectory=/opt/webdav-server
|
||||
|
||||
# Environment file
|
||||
EnvironmentFile=/opt/webdav-server/.env
|
||||
|
||||
# Command to start the service (using Gunicorn for production)
|
||||
ExecStart=/opt/webdav-server/venv/bin/gunicorn main:init_app \
|
||||
--config /opt/webdav-server/gunicorn_config.py \
|
||||
--bind 0.0.0.0:8080 \
|
||||
--worker-class aiohttp.GunicornWebWorker \
|
||||
--workers 4 \
|
||||
--access-logfile /var/log/webdav/access.log \
|
||||
--error-logfile /var/log/webdav/error.log \
|
||||
--log-level info
|
||||
|
||||
# Alternative: Run with Python directly (for development)
|
||||
# ExecStart=/opt/webdav-server/venv/bin/python /opt/webdav-server/main.py
|
||||
|
||||
# Restart policy
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65536
|
||||
LimitNPROC=4096
|
||||
|
||||
# Security hardening
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/opt/webdav-server/webdav /opt/webdav-server/logs /opt/webdav-server/backups /opt/webdav-server/webdav.db
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
|
||||
RestrictNamespaces=true
|
||||
RestrictRealtime=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
SystemCallArchitectures=native
|
||||
|
||||
# Process properties
|
||||
Nice=0
|
||||
IOSchedulingClass=best-effort
|
||||
IOSchedulingPriority=4
|
||||
|
||||
# Standard output and error
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=webdav-server
|
||||
|
||||
# Watchdog (for monitoring)
|
||||
WatchdogSec=60s
|
||||
|
||||
# Kill mode
|
||||
KillMode=mixed
|
||||
KillSignal=SIGTERM
|
||||
TimeoutStopSec=30s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
458
webdav_cli.py
Normal file
458
webdav_cli.py
Normal file
@ -0,0 +1,458 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
WebDAV Server CLI Management Tool
|
||||
Command-line interface for managing users, permissions, and server configuration
|
||||
"""
|
||||
|
||||
import sys
|
||||
import asyncio
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from getpass import getpass
|
||||
from datetime import datetime
|
||||
import sqlite3
|
||||
|
||||
# Import from main application
|
||||
from main import Database, Config
|
||||
|
||||
|
||||
class WebDAVCLI:
|
||||
"""Command-line interface for WebDAV server management"""
|
||||
|
||||
def __init__(self):
|
||||
self.db = Database(Config.DB_PATH)
|
||||
|
||||
# ========================================================================
|
||||
# User Management
|
||||
# ========================================================================
|
||||
|
||||
async def create_user(self, username: str, password: str = None,
|
||||
email: str = None, role: str = 'user'):
|
||||
"""Create a new user"""
|
||||
if not password:
|
||||
password = getpass(f"Enter password for {username}: ")
|
||||
password_confirm = getpass("Confirm password: ")
|
||||
|
||||
if password != password_confirm:
|
||||
print("❌ Passwords do not match!")
|
||||
return False
|
||||
|
||||
if len(password) < Config.PASSWORD_MIN_LENGTH:
|
||||
print(f"❌ Password must be at least {Config.PASSWORD_MIN_LENGTH} characters!")
|
||||
return False
|
||||
|
||||
try:
|
||||
user_id = await self.db.create_user(username, password, email, role)
|
||||
print(f"✅ User created successfully!")
|
||||
print(f" ID: {user_id}")
|
||||
print(f" Username: {username}")
|
||||
print(f" Email: {email or 'N/A'}")
|
||||
print(f" Role: {role}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Error creating user: {e}")
|
||||
return False
|
||||
|
||||
async def list_users(self):
|
||||
"""List all users"""
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT id, username, email, role, is_active, created_at, last_login FROM users')
|
||||
users = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
if not users:
|
||||
print("No users found.")
|
||||
return
|
||||
|
||||
print(f"\n{'ID':<5} {'Username':<20} {'Email':<30} {'Role':<10} {'Active':<8} {'Created':<20}")
|
||||
print("=" * 100)
|
||||
|
||||
for user in users:
|
||||
active = "✓" if user['is_active'] else "✗"
|
||||
created = user['created_at'][:19] if user['created_at'] else 'N/A'
|
||||
email = user['email'] or 'N/A'
|
||||
|
||||
print(f"{user['id']:<5} {user['username']:<20} {email:<30} {user['role']:<10} {active:<8} {created:<20}")
|
||||
|
||||
async def delete_user(self, username: str, force: bool = False):
|
||||
"""Delete a user"""
|
||||
if not force:
|
||||
confirm = input(f"Are you sure you want to delete user '{username}'? (yes/no): ")
|
||||
if confirm.lower() != 'yes':
|
||||
print("Deletion cancelled.")
|
||||
return False
|
||||
|
||||
try:
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('DELETE FROM users WHERE username = ?', (username,))
|
||||
deleted = cursor.rowcount > 0
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
if deleted:
|
||||
# Remove user directory
|
||||
user_dir = Path(Config.WEBDAV_ROOT) / 'users' / username
|
||||
if user_dir.exists():
|
||||
import shutil
|
||||
shutil.rmtree(user_dir)
|
||||
|
||||
print(f"✅ User '{username}' deleted successfully!")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ User '{username}' not found!")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error deleting user: {e}")
|
||||
return False
|
||||
|
||||
async def change_password(self, username: str, new_password: str = None):
|
||||
"""Change user password"""
|
||||
if not new_password:
|
||||
new_password = getpass(f"Enter new password for {username}: ")
|
||||
password_confirm = getpass("Confirm new password: ")
|
||||
|
||||
if new_password != password_confirm:
|
||||
print("❌ Passwords do not match!")
|
||||
return False
|
||||
|
||||
if len(new_password) < Config.PASSWORD_MIN_LENGTH:
|
||||
print(f"❌ Password must be at least {Config.PASSWORD_MIN_LENGTH} characters!")
|
||||
return False
|
||||
|
||||
try:
|
||||
import hashlib
|
||||
import secrets
|
||||
|
||||
salt = secrets.token_hex(16)
|
||||
password_hash = hashlib.pbkdf2_hmac('sha256', new_password.encode(), salt.encode(), 100000).hex()
|
||||
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('UPDATE users SET password_hash = ?, salt = ? WHERE username = ?',
|
||||
(password_hash, salt, username))
|
||||
updated = cursor.rowcount > 0
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
if updated:
|
||||
print(f"✅ Password changed successfully for user '{username}'!")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ User '{username}' not found!")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error changing password: {e}")
|
||||
return False
|
||||
|
||||
async def activate_user(self, username: str, active: bool = True):
|
||||
"""Activate or deactivate a user"""
|
||||
try:
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('UPDATE users SET is_active = ? WHERE username = ?',
|
||||
(1 if active else 0, username))
|
||||
updated = cursor.rowcount > 0
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
if updated:
|
||||
status = "activated" if active else "deactivated"
|
||||
print(f"✅ User '{username}' {status} successfully!")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ User '{username}' not found!")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error updating user: {e}")
|
||||
return False
|
||||
|
||||
# ========================================================================
|
||||
# Lock Management
|
||||
# ========================================================================
|
||||
|
||||
async def list_locks(self):
|
||||
"""List all active locks"""
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
SELECT l.lock_token, l.resource_path, l.lock_type, l.lock_scope,
|
||||
l.created_at, l.timeout_seconds, u.username
|
||||
FROM locks l
|
||||
LEFT JOIN users u ON l.user_id = u.id
|
||||
WHERE datetime(l.created_at, '+' || l.timeout_seconds || ' seconds') > datetime('now')
|
||||
''')
|
||||
locks = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
if not locks:
|
||||
print("No active locks found.")
|
||||
return
|
||||
|
||||
print(f"\n{'Resource':<40} {'Type':<10} {'Scope':<12} {'Owner':<15} {'Created':<20}")
|
||||
print("=" * 100)
|
||||
|
||||
for lock in locks:
|
||||
created = lock['created_at'][:19] if lock['created_at'] else 'N/A'
|
||||
owner = lock['username'] or 'Unknown'
|
||||
|
||||
print(f"{lock['resource_path']:<40} {lock['lock_type']:<10} {lock['lock_scope']:<12} {owner:<15} {created:<20}")
|
||||
|
||||
async def remove_lock(self, resource_path: str):
|
||||
"""Remove a lock from a resource"""
|
||||
try:
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('DELETE FROM locks WHERE resource_path = ?', (resource_path,))
|
||||
deleted = cursor.rowcount > 0
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
if deleted:
|
||||
print(f"✅ Lock removed from '{resource_path}'!")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ No lock found on '{resource_path}'!")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error removing lock: {e}")
|
||||
return False
|
||||
|
||||
# ========================================================================
|
||||
# Statistics and Info
|
||||
# ========================================================================
|
||||
|
||||
async def show_stats(self):
|
||||
"""Show server statistics"""
|
||||
conn = self.db.get_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# User statistics
|
||||
cursor.execute('SELECT COUNT(*) as total, SUM(is_active) as active FROM users')
|
||||
user_stats = cursor.fetchone()
|
||||
|
||||
# Lock statistics
|
||||
cursor.execute('''
|
||||
SELECT COUNT(*) as total FROM locks
|
||||
WHERE datetime(created_at, '+' || timeout_seconds || ' seconds') > datetime('now')
|
||||
''')
|
||||
lock_stats = cursor.fetchone()
|
||||
|
||||
# Session statistics
|
||||
cursor.execute('''
|
||||
SELECT COUNT(*) as total FROM sessions
|
||||
WHERE datetime(expires_at) > datetime('now')
|
||||
''')
|
||||
session_stats = cursor.fetchone()
|
||||
|
||||
# Properties statistics
|
||||
cursor.execute('SELECT COUNT(*) as total FROM properties')
|
||||
prop_stats = cursor.fetchone()
|
||||
|
||||
conn.close()
|
||||
|
||||
# Storage statistics
|
||||
webdav_root = Path(Config.WEBDAV_ROOT)
|
||||
if webdav_root.exists():
|
||||
total_size = sum(f.stat().st_size for f in webdav_root.rglob('*') if f.is_file())
|
||||
total_files = sum(1 for f in webdav_root.rglob('*') if f.is_file())
|
||||
else:
|
||||
total_size = 0
|
||||
total_files = 0
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("WebDAV Server Statistics")
|
||||
print("=" * 60)
|
||||
print(f"\n📊 Users:")
|
||||
print(f" Total: {user_stats['total']}")
|
||||
print(f" Active: {user_stats['active']}")
|
||||
print(f" Inactive: {user_stats['total'] - user_stats['active']}")
|
||||
|
||||
print(f"\n🔒 Locks:")
|
||||
print(f" Active: {lock_stats['total']}")
|
||||
|
||||
print(f"\n🔑 Sessions:")
|
||||
print(f" Active: {session_stats['total']}")
|
||||
|
||||
print(f"\n📝 Properties:")
|
||||
print(f" Total: {prop_stats['total']}")
|
||||
|
||||
print(f"\n💾 Storage:")
|
||||
print(f" Total files: {total_files:,}")
|
||||
print(f" Total size: {total_size:,} bytes ({total_size / 1024 / 1024:.2f} MB)")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
# ========================================================================
|
||||
# Database Management
|
||||
# ========================================================================
|
||||
|
||||
async def backup_database(self, output_path: str = None):
|
||||
"""Backup the database"""
|
||||
if not output_path:
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
output_path = f"./backups/webdav_backup_{timestamp}.db"
|
||||
|
||||
try:
|
||||
import shutil
|
||||
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(Config.DB_PATH, output_path)
|
||||
print(f"✅ Database backed up to: {output_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Error backing up database: {e}")
|
||||
return False
|
||||
|
||||
async def restore_database(self, backup_path: str):
|
||||
"""Restore database from backup"""
|
||||
if not Path(backup_path).exists():
|
||||
print(f"❌ Backup file not found: {backup_path}")
|
||||
return False
|
||||
|
||||
confirm = input("⚠️ This will overwrite the current database. Continue? (yes/no): ")
|
||||
if confirm.lower() != 'yes':
|
||||
print("Restore cancelled.")
|
||||
return False
|
||||
|
||||
try:
|
||||
import shutil
|
||||
shutil.copy2(backup_path, Config.DB_PATH)
|
||||
print(f"✅ Database restored from: {backup_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"❌ Error restoring database: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CLI Command Handlers
|
||||
# ============================================================================
|
||||
|
||||
async def main():
|
||||
"""Main CLI entry point"""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='WebDAV Server Management CLI',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
%(prog)s user create john --email john@example.com
|
||||
%(prog)s user list
|
||||
%(prog)s user delete john
|
||||
%(prog)s user password john
|
||||
%(prog)s lock list
|
||||
%(prog)s stats
|
||||
%(prog)s backup
|
||||
"""
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
||||
|
||||
# User commands
|
||||
user_parser = subparsers.add_parser('user', help='User management')
|
||||
user_subparsers = user_parser.add_subparsers(dest='user_command')
|
||||
|
||||
# Create user
|
||||
create_parser = user_subparsers.add_parser('create', help='Create a new user')
|
||||
create_parser.add_argument('username', help='Username')
|
||||
create_parser.add_argument('--password', help='Password (will prompt if not provided)')
|
||||
create_parser.add_argument('--email', help='Email address')
|
||||
create_parser.add_argument('--role', default='user', choices=['user', 'admin'], help='User role')
|
||||
|
||||
# List users
|
||||
user_subparsers.add_parser('list', help='List all users')
|
||||
|
||||
# Delete user
|
||||
delete_parser = user_subparsers.add_parser('delete', help='Delete a user')
|
||||
delete_parser.add_argument('username', help='Username to delete')
|
||||
delete_parser.add_argument('--force', action='store_true', help='Skip confirmation')
|
||||
|
||||
# Change password
|
||||
password_parser = user_subparsers.add_parser('password', help='Change user password')
|
||||
password_parser.add_argument('username', help='Username')
|
||||
password_parser.add_argument('--new-password', help='New password (will prompt if not provided)')
|
||||
|
||||
# Activate/deactivate user
|
||||
activate_parser = user_subparsers.add_parser('activate', help='Activate a user')
|
||||
activate_parser.add_argument('username', help='Username')
|
||||
|
||||
deactivate_parser = user_subparsers.add_parser('deactivate', help='Deactivate a user')
|
||||
deactivate_parser.add_argument('username', help='Username')
|
||||
|
||||
# Lock commands
|
||||
lock_parser = subparsers.add_parser('lock', help='Lock management')
|
||||
lock_subparsers = lock_parser.add_subparsers(dest='lock_command')
|
||||
|
||||
lock_subparsers.add_parser('list', help='List all active locks')
|
||||
|
||||
remove_parser = lock_subparsers.add_parser('remove', help='Remove a lock')
|
||||
remove_parser.add_argument('resource', help='Resource path')
|
||||
|
||||
# Statistics
|
||||
subparsers.add_parser('stats', help='Show server statistics')
|
||||
|
||||
# Backup
|
||||
backup_parser = subparsers.add_parser('backup', help='Backup database')
|
||||
backup_parser.add_argument('--output', help='Output path for backup')
|
||||
|
||||
# Restore
|
||||
restore_parser = subparsers.add_parser('restore', help='Restore database from backup')
|
||||
restore_parser.add_argument('backup', help='Path to backup file')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.command:
|
||||
parser.print_help()
|
||||
return
|
||||
|
||||
cli = WebDAVCLI()
|
||||
|
||||
# Execute command
|
||||
try:
|
||||
if args.command == 'user':
|
||||
if args.user_command == 'create':
|
||||
await cli.create_user(args.username, args.password, args.email, args.role)
|
||||
elif args.user_command == 'list':
|
||||
await cli.list_users()
|
||||
elif args.user_command == 'delete':
|
||||
await cli.delete_user(args.username, args.force)
|
||||
elif args.user_command == 'password':
|
||||
await cli.change_password(args.username, getattr(args, 'new_password', None))
|
||||
elif args.user_command == 'activate':
|
||||
await cli.activate_user(args.username, True)
|
||||
elif args.user_command == 'deactivate':
|
||||
await cli.activate_user(args.username, False)
|
||||
else:
|
||||
user_parser.print_help()
|
||||
|
||||
elif args.command == 'lock':
|
||||
if args.lock_command == 'list':
|
||||
await cli.list_locks()
|
||||
elif args.lock_command == 'remove':
|
||||
await cli.remove_lock(args.resource)
|
||||
else:
|
||||
lock_parser.print_help()
|
||||
|
||||
elif args.command == 'stats':
|
||||
await cli.show_stats()
|
||||
|
||||
elif args.command == 'backup':
|
||||
await cli.backup_database(args.output)
|
||||
|
||||
elif args.command == 'restore':
|
||||
await cli.restore_database(args.backup)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n\nOperation cancelled.")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(main())
|
||||
Loading…
Reference in New Issue
Block a user