- Created complete documentation in docs/ directory - Added PROJECT_OVERVIEW.md with feature highlights and getting started guide - Added ARCHITECTURE.md with system design and technical details - Added SECURITY.md with comprehensive security implementation guide - Added DEVELOPMENT.md with development workflows and best practices - Added DEPLOYMENT.md with production deployment instructions - Added API.md with complete REST API documentation - Added CONTRIBUTING.md with contribution guidelines - Added CHANGELOG.md with version history and migration notes - Reorganized all documentation files into docs/ directory for better organization - Updated README.md with proper documentation links and quick navigation - Enhanced project structure with professional documentation standards
701 lines
15 KiB
Markdown
701 lines
15 KiB
Markdown
# Deployment Guide
|
|
|
|
This guide covers various deployment scenarios for EasyStream, from development to production environments.
|
|
|
|
## Quick Start (Docker)
|
|
|
|
### Prerequisites
|
|
- Docker 20.10+
|
|
- Docker Compose 2.0+
|
|
- 4GB+ RAM
|
|
- 20GB+ storage
|
|
|
|
### Basic Deployment
|
|
```bash
|
|
# Clone repository
|
|
git clone <repository-url>
|
|
cd easystream
|
|
|
|
# Configure environment
|
|
cp .env.example .env
|
|
# Edit .env with your settings
|
|
|
|
# Start services
|
|
docker-compose up -d --build
|
|
|
|
# Verify deployment
|
|
curl http://localhost:8083
|
|
```
|
|
|
|
## Production Deployment
|
|
|
|
### System Requirements
|
|
|
|
#### Minimum Requirements
|
|
- **CPU**: 2 cores
|
|
- **RAM**: 4GB
|
|
- **Storage**: 50GB SSD
|
|
- **Network**: 100Mbps
|
|
|
|
#### Recommended Requirements
|
|
- **CPU**: 4+ cores
|
|
- **RAM**: 8GB+
|
|
- **Storage**: 200GB+ SSD
|
|
- **Network**: 1Gbps
|
|
|
|
### Environment Configuration
|
|
|
|
#### Environment Variables
|
|
```bash
|
|
# Application
|
|
MAIN_URL=https://your-domain.com
|
|
DEBUG_MODE=false
|
|
ENVIRONMENT=production
|
|
|
|
# Database
|
|
DB_HOST=db
|
|
DB_NAME=easystream
|
|
DB_USER=easystream
|
|
DB_PASS=secure_password_here
|
|
|
|
# Security
|
|
SESSION_SECURE=true
|
|
CSRF_PROTECTION=enabled
|
|
RATE_LIMITING=enabled
|
|
|
|
# Storage
|
|
UPLOAD_MAX_SIZE=2G
|
|
STORAGE_DRIVER=local
|
|
# For S3: STORAGE_DRIVER=s3
|
|
# AWS_ACCESS_KEY_ID=your_key
|
|
# AWS_SECRET_ACCESS_KEY=your_secret
|
|
# AWS_DEFAULT_REGION=us-east-1
|
|
# AWS_BUCKET=your-bucket
|
|
|
|
# Email
|
|
MAIL_DRIVER=smtp
|
|
MAIL_HOST=smtp.your-provider.com
|
|
MAIL_PORT=587
|
|
MAIL_USERNAME=your_email@domain.com
|
|
MAIL_PASSWORD=your_password
|
|
MAIL_ENCRYPTION=tls
|
|
|
|
# Live Streaming
|
|
SRS_RTMP_PORT=1935
|
|
SRS_HTTP_PORT=8080
|
|
HLS_SEGMENT_DURATION=10
|
|
HLS_WINDOW_SIZE=60
|
|
|
|
# Monitoring
|
|
LOG_LEVEL=warning
|
|
LOG_DRIVER=file
|
|
# For centralized logging: LOG_DRIVER=syslog
|
|
```
|
|
|
|
### Docker Production Setup
|
|
|
|
#### Production Docker Compose
|
|
```yaml
|
|
# docker-compose.prod.yml
|
|
version: '3.8'
|
|
|
|
services:
|
|
php:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.php
|
|
target: production
|
|
environment:
|
|
- ENVIRONMENT=production
|
|
- DEBUG_MODE=false
|
|
volumes:
|
|
- ./f_data:/var/www/html/f_data
|
|
- uploads:/var/www/html/f_data/uploads
|
|
- hls:/var/www/html/hls
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- db
|
|
- redis
|
|
|
|
caddy:
|
|
image: caddy:2-alpine
|
|
ports:
|
|
- "80:80"
|
|
- "443:443"
|
|
volumes:
|
|
- ./Caddyfile.prod:/etc/caddy/Caddyfile
|
|
- caddy_data:/data
|
|
- caddy_config:/config
|
|
- hls:/srv/hls:ro
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- php
|
|
|
|
db:
|
|
image: mariadb:10.11
|
|
environment:
|
|
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASS}
|
|
MYSQL_DATABASE: ${DB_NAME}
|
|
MYSQL_USER: ${DB_USER}
|
|
MYSQL_PASSWORD: ${DB_PASS}
|
|
volumes:
|
|
- db_data:/var/lib/mysql
|
|
- ./deploy/mysql.cnf:/etc/mysql/conf.d/custom.cnf
|
|
restart: unless-stopped
|
|
command: --innodb-buffer-pool-size=1G
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
volumes:
|
|
- redis_data:/data
|
|
restart: unless-stopped
|
|
command: redis-server --appendonly yes
|
|
|
|
srs:
|
|
image: ossrs/srs:5
|
|
ports:
|
|
- "1935:1935"
|
|
- "8080:8080"
|
|
volumes:
|
|
- ./deploy/srs.prod.conf:/usr/local/srs/conf/srs.conf
|
|
- hls:/usr/local/srs/objs/nginx/html/hls
|
|
- recordings:/usr/local/srs/objs/nginx/html/rec
|
|
restart: unless-stopped
|
|
|
|
cron:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile.cron
|
|
volumes:
|
|
- ./f_data:/var/www/html/f_data
|
|
- uploads:/var/www/html/f_data/uploads
|
|
restart: unless-stopped
|
|
depends_on:
|
|
- db
|
|
- redis
|
|
|
|
volumes:
|
|
db_data:
|
|
redis_data:
|
|
caddy_data:
|
|
caddy_config:
|
|
uploads:
|
|
hls:
|
|
recordings:
|
|
```
|
|
|
|
#### Production Caddyfile
|
|
```caddyfile
|
|
# Caddyfile.prod
|
|
{
|
|
email your-email@domain.com
|
|
admin off
|
|
}
|
|
|
|
your-domain.com {
|
|
# Security headers
|
|
header {
|
|
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
|
|
X-Content-Type-Options "nosniff"
|
|
X-Frame-Options "DENY"
|
|
X-XSS-Protection "1; mode=block"
|
|
Referrer-Policy "strict-origin-when-cross-origin"
|
|
Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; media-src 'self' blob:; connect-src 'self' wss:"
|
|
}
|
|
|
|
# Rate limiting
|
|
rate_limit {
|
|
zone static {
|
|
key {remote_host}
|
|
events 100
|
|
window 1m
|
|
}
|
|
zone api {
|
|
key {remote_host}
|
|
events 1000
|
|
window 1h
|
|
}
|
|
}
|
|
|
|
# HLS streaming
|
|
handle /hls/* {
|
|
root * /srv
|
|
file_server {
|
|
precompressed gzip br
|
|
}
|
|
header Cache-Control "public, max-age=10"
|
|
}
|
|
|
|
# Static assets
|
|
handle /f_scripts/* /f_templates/* {
|
|
root * /var/www/html
|
|
file_server {
|
|
precompressed gzip br
|
|
}
|
|
header Cache-Control "public, max-age=31536000"
|
|
}
|
|
|
|
# API endpoints
|
|
handle /api/* {
|
|
rate_limit api
|
|
reverse_proxy php:9000 {
|
|
transport fastcgi {
|
|
split .php
|
|
}
|
|
}
|
|
}
|
|
|
|
# Main application
|
|
handle {
|
|
root * /var/www/html
|
|
php_fastcgi php:9000
|
|
file_server
|
|
}
|
|
|
|
# Logging
|
|
log {
|
|
output file /var/log/caddy/access.log {
|
|
roll_size 100mb
|
|
roll_keep 5
|
|
}
|
|
format json
|
|
}
|
|
}
|
|
```
|
|
|
|
### Manual Installation
|
|
|
|
#### Server Setup (Ubuntu 22.04)
|
|
```bash
|
|
# Update system
|
|
sudo apt update && sudo apt upgrade -y
|
|
|
|
# Install PHP 8.2
|
|
sudo add-apt-repository ppa:ondrej/php -y
|
|
sudo apt update
|
|
sudo apt install -y php8.2 php8.2-fpm php8.2-mysql php8.2-gd php8.2-curl \
|
|
php8.2-mbstring php8.2-xml php8.2-zip php8.2-bcmath php8.2-intl
|
|
|
|
# Install MariaDB
|
|
sudo apt install -y mariadb-server
|
|
sudo mysql_secure_installation
|
|
|
|
# Install Nginx
|
|
sudo apt install -y nginx
|
|
|
|
# Install FFmpeg
|
|
sudo apt install -y ffmpeg
|
|
|
|
# Install SRS
|
|
wget https://github.com/ossrs/srs/releases/download/v5.0.156/srs-server-5.0.156-ubuntu20-amd64.deb
|
|
sudo dpkg -i srs-server-5.0.156-ubuntu20-amd64.deb
|
|
```
|
|
|
|
#### Database Setup
|
|
```sql
|
|
-- Create database and user
|
|
CREATE DATABASE easystream CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
CREATE USER 'easystream'@'localhost' IDENTIFIED BY 'secure_password';
|
|
GRANT ALL PRIVILEGES ON easystream.* TO 'easystream'@'localhost';
|
|
FLUSH PRIVILEGES;
|
|
|
|
-- Import schema
|
|
mysql -u easystream -p easystream < __install/easystream.sql
|
|
```
|
|
|
|
#### Nginx Configuration
|
|
```nginx
|
|
# /etc/nginx/sites-available/easystream
|
|
server {
|
|
listen 80;
|
|
server_name your-domain.com;
|
|
return 301 https://$server_name$request_uri;
|
|
}
|
|
|
|
server {
|
|
listen 443 ssl http2;
|
|
server_name your-domain.com;
|
|
root /var/www/easystream;
|
|
index index.php index.html;
|
|
|
|
# SSL Configuration
|
|
ssl_certificate /path/to/certificate.crt;
|
|
ssl_certificate_key /path/to/private.key;
|
|
ssl_protocols TLSv1.2 TLSv1.3;
|
|
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
|
|
ssl_prefer_server_ciphers off;
|
|
|
|
# Security headers
|
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
|
add_header X-Content-Type-Options "nosniff" always;
|
|
add_header X-Frame-Options "DENY" always;
|
|
add_header X-XSS-Protection "1; mode=block" always;
|
|
|
|
# Rate limiting
|
|
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
|
|
limit_req_zone $binary_remote_addr zone=general:10m rate=1r/s;
|
|
|
|
# HLS streaming
|
|
location /hls/ {
|
|
alias /var/srs/hls/;
|
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
|
add_header Access-Control-Allow-Origin "*";
|
|
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
|
|
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range";
|
|
}
|
|
|
|
# API endpoints
|
|
location /api/ {
|
|
limit_req zone=api burst=20 nodelay;
|
|
try_files $uri $uri/ /index.php?$query_string;
|
|
}
|
|
|
|
# PHP processing
|
|
location ~ \.php$ {
|
|
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
|
|
fastcgi_index index.php;
|
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
|
include fastcgi_params;
|
|
}
|
|
|
|
# Static files
|
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
expires 1y;
|
|
add_header Cache-Control "public, immutable";
|
|
}
|
|
|
|
# Security
|
|
location ~ /\. {
|
|
deny all;
|
|
}
|
|
|
|
location ~ /(f_data|__install|tests)/ {
|
|
deny all;
|
|
}
|
|
}
|
|
```
|
|
|
|
#### PHP-FPM Configuration
|
|
```ini
|
|
; /etc/php/8.2/fpm/pool.d/easystream.conf
|
|
[easystream]
|
|
user = www-data
|
|
group = www-data
|
|
listen = /var/run/php/php8.2-fpm-easystream.sock
|
|
listen.owner = www-data
|
|
listen.group = www-data
|
|
listen.mode = 0660
|
|
|
|
pm = dynamic
|
|
pm.max_children = 50
|
|
pm.start_servers = 5
|
|
pm.min_spare_servers = 5
|
|
pm.max_spare_servers = 35
|
|
pm.max_requests = 1000
|
|
|
|
; PHP settings
|
|
php_admin_value[upload_max_filesize] = 2G
|
|
php_admin_value[post_max_size] = 2G
|
|
php_admin_value[max_execution_time] = 300
|
|
php_admin_value[memory_limit] = 512M
|
|
```
|
|
|
|
### Cloud Deployment
|
|
|
|
#### AWS Deployment
|
|
```bash
|
|
# Using AWS ECS with Fargate
|
|
aws ecs create-cluster --cluster-name easystream-prod
|
|
|
|
# Create task definition
|
|
aws ecs register-task-definition --cli-input-json file://ecs-task-definition.json
|
|
|
|
# Create service
|
|
aws ecs create-service \
|
|
--cluster easystream-prod \
|
|
--service-name easystream-web \
|
|
--task-definition easystream:1 \
|
|
--desired-count 2 \
|
|
--launch-type FARGATE \
|
|
--network-configuration "awsvpcConfiguration={subnets=[subnet-12345],securityGroups=[sg-12345],assignPublicIp=ENABLED}"
|
|
```
|
|
|
|
#### Google Cloud Platform
|
|
```bash
|
|
# Deploy to Cloud Run
|
|
gcloud run deploy easystream \
|
|
--image gcr.io/PROJECT_ID/easystream \
|
|
--platform managed \
|
|
--region us-central1 \
|
|
--allow-unauthenticated \
|
|
--memory 2Gi \
|
|
--cpu 2 \
|
|
--max-instances 10
|
|
```
|
|
|
|
#### DigitalOcean App Platform
|
|
```yaml
|
|
# .do/app.yaml
|
|
name: easystream
|
|
services:
|
|
- name: web
|
|
source_dir: /
|
|
github:
|
|
repo: your-username/easystream
|
|
branch: main
|
|
run_command: php-fpm
|
|
environment_slug: php
|
|
instance_count: 2
|
|
instance_size_slug: professional-xs
|
|
envs:
|
|
- key: ENVIRONMENT
|
|
value: production
|
|
- key: DB_HOST
|
|
value: ${db.HOSTNAME}
|
|
- key: DB_NAME
|
|
value: ${db.DATABASE}
|
|
- key: DB_USER
|
|
value: ${db.USERNAME}
|
|
- key: DB_PASS
|
|
value: ${db.PASSWORD}
|
|
|
|
databases:
|
|
- name: db
|
|
engine: MYSQL
|
|
version: "8"
|
|
size: db-s-1vcpu-1gb
|
|
```
|
|
|
|
### SSL/TLS Configuration
|
|
|
|
#### Let's Encrypt with Certbot
|
|
```bash
|
|
# Install Certbot
|
|
sudo apt install -y certbot python3-certbot-nginx
|
|
|
|
# Obtain certificate
|
|
sudo certbot --nginx -d your-domain.com
|
|
|
|
# Auto-renewal
|
|
sudo crontab -e
|
|
# Add: 0 12 * * * /usr/bin/certbot renew --quiet
|
|
```
|
|
|
|
#### Cloudflare SSL
|
|
```bash
|
|
# Configure Cloudflare origin certificates
|
|
# Download origin certificate and key
|
|
# Update Nginx/Caddy configuration with certificate paths
|
|
```
|
|
|
|
### Monitoring and Logging
|
|
|
|
#### System Monitoring
|
|
```bash
|
|
# Install monitoring tools
|
|
sudo apt install -y htop iotop nethogs
|
|
|
|
# Install Prometheus Node Exporter
|
|
wget https://github.com/prometheus/node_exporter/releases/download/v1.6.1/node_exporter-1.6.1.linux-amd64.tar.gz
|
|
tar xvfz node_exporter-1.6.1.linux-amd64.tar.gz
|
|
sudo cp node_exporter-1.6.1.linux-amd64/node_exporter /usr/local/bin/
|
|
sudo useradd --no-create-home --shell /bin/false node_exporter
|
|
sudo chown node_exporter:node_exporter /usr/local/bin/node_exporter
|
|
```
|
|
|
|
#### Log Management
|
|
```bash
|
|
# Configure log rotation
|
|
sudo tee /etc/logrotate.d/easystream << EOF
|
|
/var/www/easystream/f_data/logs/*.log {
|
|
daily
|
|
missingok
|
|
rotate 30
|
|
compress
|
|
delaycompress
|
|
notifempty
|
|
create 644 www-data www-data
|
|
}
|
|
EOF
|
|
```
|
|
|
|
### Backup Strategy
|
|
|
|
#### Database Backup
|
|
```bash
|
|
#!/bin/bash
|
|
# backup-db.sh
|
|
DATE=$(date +%Y%m%d_%H%M%S)
|
|
BACKUP_DIR="/backups/database"
|
|
mkdir -p $BACKUP_DIR
|
|
|
|
mysqldump -u easystream -p easystream | gzip > $BACKUP_DIR/easystream_$DATE.sql.gz
|
|
|
|
# Keep only last 30 days
|
|
find $BACKUP_DIR -name "*.sql.gz" -mtime +30 -delete
|
|
```
|
|
|
|
#### File Backup
|
|
```bash
|
|
#!/bin/bash
|
|
# backup-files.sh
|
|
DATE=$(date +%Y%m%d_%H%M%S)
|
|
BACKUP_DIR="/backups/files"
|
|
SOURCE_DIR="/var/www/easystream/f_data"
|
|
|
|
mkdir -p $BACKUP_DIR
|
|
tar -czf $BACKUP_DIR/files_$DATE.tar.gz -C $SOURCE_DIR .
|
|
|
|
# Keep only last 7 days
|
|
find $BACKUP_DIR -name "*.tar.gz" -mtime +7 -delete
|
|
```
|
|
|
|
### Performance Optimization
|
|
|
|
#### Database Optimization
|
|
```sql
|
|
-- MySQL/MariaDB configuration
|
|
[mysqld]
|
|
innodb_buffer_pool_size = 2G
|
|
innodb_log_file_size = 256M
|
|
innodb_flush_log_at_trx_commit = 2
|
|
query_cache_size = 128M
|
|
query_cache_type = 1
|
|
max_connections = 200
|
|
```
|
|
|
|
#### PHP Optimization
|
|
```ini
|
|
; php.ini optimizations
|
|
opcache.enable=1
|
|
opcache.memory_consumption=256
|
|
opcache.interned_strings_buffer=16
|
|
opcache.max_accelerated_files=10000
|
|
opcache.revalidate_freq=2
|
|
opcache.fast_shutdown=1
|
|
|
|
realpath_cache_size=4096K
|
|
realpath_cache_ttl=600
|
|
```
|
|
|
|
#### CDN Configuration
|
|
```bash
|
|
# Configure CloudFlare or AWS CloudFront
|
|
# Cache static assets: images, videos, CSS, JS
|
|
# Set appropriate cache headers
|
|
# Enable compression (gzip/brotli)
|
|
```
|
|
|
|
### Security Hardening
|
|
|
|
#### Firewall Configuration
|
|
```bash
|
|
# UFW firewall rules
|
|
sudo ufw default deny incoming
|
|
sudo ufw default allow outgoing
|
|
sudo ufw allow ssh
|
|
sudo ufw allow 80/tcp
|
|
sudo ufw allow 443/tcp
|
|
sudo ufw allow 1935/tcp # RTMP
|
|
sudo ufw enable
|
|
```
|
|
|
|
#### Fail2Ban Setup
|
|
```bash
|
|
# Install Fail2Ban
|
|
sudo apt install -y fail2ban
|
|
|
|
# Configure jail
|
|
sudo tee /etc/fail2ban/jail.local << EOF
|
|
[DEFAULT]
|
|
bantime = 3600
|
|
findtime = 600
|
|
maxretry = 5
|
|
|
|
[nginx-http-auth]
|
|
enabled = true
|
|
filter = nginx-http-auth
|
|
logpath = /var/log/nginx/error.log
|
|
|
|
[nginx-limit-req]
|
|
enabled = true
|
|
filter = nginx-limit-req
|
|
logpath = /var/log/nginx/error.log
|
|
maxretry = 10
|
|
EOF
|
|
|
|
sudo systemctl restart fail2ban
|
|
```
|
|
|
|
### Troubleshooting
|
|
|
|
#### Common Issues
|
|
1. **502 Bad Gateway**: Check PHP-FPM status and configuration
|
|
2. **Database Connection**: Verify credentials and network connectivity
|
|
3. **File Permissions**: Ensure www-data has proper permissions
|
|
4. **Memory Issues**: Increase PHP memory limit and server RAM
|
|
5. **Streaming Issues**: Check SRS configuration and network ports
|
|
|
|
#### Debug Commands
|
|
```bash
|
|
# Check service status
|
|
sudo systemctl status nginx php8.2-fpm mariadb
|
|
|
|
# View logs
|
|
sudo tail -f /var/log/nginx/error.log
|
|
sudo tail -f /var/www/easystream/f_data/logs/error_$(date +%Y-%m-%d).log
|
|
|
|
# Test database connection
|
|
mysql -u easystream -p -h localhost easystream
|
|
|
|
# Check disk space
|
|
df -h
|
|
|
|
# Monitor processes
|
|
htop
|
|
```
|
|
|
|
### Maintenance
|
|
|
|
#### Regular Maintenance Tasks
|
|
```bash
|
|
#!/bin/bash
|
|
# maintenance.sh - Run weekly
|
|
|
|
# Update system packages
|
|
sudo apt update && sudo apt upgrade -y
|
|
|
|
# Clean up old logs
|
|
find /var/www/easystream/f_data/logs -name "*.log" -mtime +30 -delete
|
|
|
|
# Optimize database
|
|
mysql -u easystream -p -e "OPTIMIZE TABLE easystream.videos, easystream.users, easystream.comments;"
|
|
|
|
# Clear application cache
|
|
rm -rf /var/www/easystream/f_data/cache/*
|
|
|
|
# Restart services
|
|
sudo systemctl restart php8.2-fpm nginx
|
|
```
|
|
|
|
#### Health Checks
|
|
```bash
|
|
#!/bin/bash
|
|
# health-check.sh - Run every 5 minutes
|
|
|
|
# Check web server
|
|
if ! curl -f http://localhost > /dev/null 2>&1; then
|
|
echo "Web server down" | mail -s "EasyStream Alert" admin@domain.com
|
|
fi
|
|
|
|
# Check database
|
|
if ! mysql -u easystream -p -e "SELECT 1" > /dev/null 2>&1; then
|
|
echo "Database down" | mail -s "EasyStream Alert" admin@domain.com
|
|
fi
|
|
|
|
# Check disk space
|
|
DISK_USAGE=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')
|
|
if [ $DISK_USAGE -gt 90 ]; then
|
|
echo "Disk usage: ${DISK_USAGE}%" | mail -s "EasyStream Alert" admin@domain.com
|
|
fi
|
|
``` |