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