1 Commits

Author SHA1 Message Date
102c108074 WIP 2025-05-06 11:36:10 +08:00
20 changed files with 1196 additions and 767 deletions

View File

@@ -1,13 +1,5 @@
IPTV_ENV=dev
KEYDB_UID=1000
KEYDB_GID=1000
KEYDB_PORT=6379
KEYDB_USERNAME=
KEYDB_PASSWORD=
CHECKER_DB=0
CHECKER_TTL=1800
CHECKER_WAIT=60
CHECKER_INIFILE=/app/playlists.ini
CHECKER_TAGFILE=/app/channels.json
REDIS_UID=1000
REDIS_GID=1000
REDIS_PORT=6379

7
.gitignore vendored
View File

@@ -1,19 +1,18 @@
/.idea/
/.vscode/
/docker/keydb/data/*
/log/**/*
/iptvc/
/web/
/docs/
/playlists/
/tools/
/.profile/
/tmp/
.env
*.log
.env
*.m3u
*.m3u.*
*.m3u8
*.m3u8.*
*.rdb
!/**/.gitkeep

View File

@@ -1,58 +1,41 @@
# Инфраструктурный слой проекта m3u.su
# Инфраструктурный слой проекта iptv.axenov.dev
Docker-окружение для работы проекта m3u.su.
> **Адрес**: https://iptv.axenov.dev
> **FAQ**: https://iptv.axenov.dev/faq
> **Исходный код**: https://git.axenov.dev/IPTV
> **Веб-сайт:** [m3u.su](https://m3u.su)
> **Документация:** [m3u.su/docs](https://m3u.su/docs)
> Исходный код: [git.axenov.dev/IPTV](https://git.axenov.dev/IPTV)
> Telegram-канал: [@iptv_aggregator](https://t.me/iptv_aggregator)
> Обсуждение: [@iptv_aggregator_chat](https://t.me/iptv_aggregator_chat)
> Бот: [@iptv_aggregator_bot](https://t.me/iptv_aggregator_bot)
Содержит docker-окружение для запуска проекта iptv.axenov.dev.
## Использованный стек
* [docker compose](https://docs.docker.com/compose/)
* [php8.4-fpm](https://www.php.net/releases/8.4/ru.php)
* [php8.3-fpm](https://www.php.net/releases/8.3/ru.php)
* [nginx](https://nginx.org/ru/)
* [keydb](https://docs.keydb.dev/docs/)
* [iptvc](https://git.axenov.dev/IPTV/iptvc)
* bash
## Установка и настройка
```
wget -O - https://git.axenov.dev/IPTV/iptv-docker/raw/branch/master/iptv | bash -s - init
git clone https://git.axenov.dev/IPTV/docker.git iptv
cp .env.example .env
git clone https://git.axenov.dev/IPTV/web.git
cp web/.env.example web/.env
docker exec -it iptv-main composer i
docker compose up -d --build
```
##рипт [`iptv`](./iptv)
### Описание переменных окружения
Это инструмент, который позволяет быстро управлять локальной средой `lis-docker`:
* инициализировать с нуля, как в примере выше;
* управлять образами и контейнерами среды.
* `IPTV_ENV` -- окружение для развёртывания: это имена директорий и/или префиксы имён конфигов, которые будут проброшены в контейнеры;
* `REDIS_UID`, `REDIS_GID` -- ID пользователя/группы для разрешения владельца файлов и директорий keydb;
* `REDIS_PORT` -- порт keydb, который будет проброшен на хост.
> Управление средой не всегда удобно через команды git и docker, поэтому рекомендуется использовать `./iptv`.
Набери `./iptv help` для справки по использованию.
При доработке используй [линтер](https://www.shellcheck.net): `shellcheck -s bash iptv`
## Описание переменных окружения
* `IPTV_ENV` — окружение для развёртывания: это имена директорий и/или префиксы имён конфигов, которые будут проброшены в контейнеры;
* `KEYDB_UID`, `KEYDB_GID` — ID пользователя/группы для разрешения владельца файлов и директорий keydb;
* `KEYDB_PORT` — порт keydb, который будет проброшен на хост.
* `KEYDB_USERNAME`, `KEYDB_PASSWORD` — реквизиты доступа к keydb;
* `CHECKER_DB` — БД keydb для хранения кеша проверенных плейлистов;
* `CHECKER_TTL` — время жизни кеша проверенных плейлистов;
* `CHECKER_WAIT` — кол-во секунд между запусками iptvc;
* `CHECKER_INIFILE` — путь к файлу списка плейлистов внутри контейнера;
* `CHECKER_TAGFILE` — путь к файлу списка тегов внутри контейнера.
## Reverse-proxy
### Reverse-proxy
На сервере опционально можно настроить реверс-прокси до контейнера веб-сервиса, например, чтобы настроить доступ по доменному имени, изменить порт, подключить SSL-сертификаты или др.
### Apache
#### Apache
Если на сервере, на котором запускаются контейнеры, стоит apache2, то, чтобы использовать его как реверс-прокси, нужно:
@@ -101,7 +84,7 @@ $ # для подгрузки включенных модулей выполни
$ sudo systemctl restart apache2
```
### Nginx
#### Nginx
```
$ sudo nano /etc/nginx/sites-available/iptv.conf

View File

@@ -1,7 +1,5 @@
name: iptv
networks:
iptv-network:
iptv:
driver: bridge
x-common-attributes: &common-attributes
@@ -14,81 +12,67 @@ x-common-attributes: &common-attributes
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
networks:
- iptv-network
- iptv
services:
nginx:
<<: *common-attributes
container_name: iptv-nginx
image: nginx:latest
pull_policy: always
volumes:
- ./docker/nginx/vhost.conf:/etc/nginx/conf.d/default.conf:ro
- ./log/nginx:/var/log/nginx:rw
- ./web:/var/www:ro
ports:
- 3000:80
depends_on:
- web
keydb:
<<: *common-attributes
container_name: iptv-keydb
image: eqalpha/keydb:latest
pull_policy: always
user: ${KEYDB_UID}:${KEYDB_GID}
entrypoint: ["sh", "/entrypoint.sh"]
build:
dockerfile: docker/keydb/dockerfile
user: "${REDIS_UID}:${REDIS_GID}"
volumes:
- ./docker/keydb/entrypoint.sh:/entrypoint.sh
- ./docker/keydb/keydb.conf:/etc/keydb/keydb.conf
- ./docker/keydb/data/:/data:rw
- ./log/keydb:/var/log/keydb/:rw
ports:
- ${KEYDB_PORT:-6379}:6379
- "${REDIS_PORT:-6379}:6379"
web:
php-main:
<<: *common-attributes
container_name: iptv-web
build:
context: ./web
dockerfile: Dockerfile
target: iptv-web-${IPTV_ENV}
container_name: iptv-main
environment:
- PHP_IDE_CONFIG=serverName=iptv.local
build:
dockerfile: docker/php/${IPTV_ENV}/dockerfile.main
volumes:
- ./web/docker/${IPTV_ENV}/www.conf:/usr/local/etc/php-fpm.d/www.conf:ro
- ./web/docker/${IPTV_ENV}/php.ini:/usr/local/etc/php/conf.d/php.ini:ro
- ./playlists/playlists.ini:/var/www/config/playlists.ini
- ./docker/php/${IPTV_ENV}/www.conf:/usr/local/etc/php-fpm.d/www.conf:ro
- ./docker/php/${IPTV_ENV}/php.ini:/usr/local/etc/php/conf.d/php.ini:ro
- ./log/php:/var/log/php:rw
- ./web:/var/www:rw
depends_on:
- keydb
checker:
php-cron:
<<: *common-attributes
container_name: iptv-checker
image: git.axenov.dev/iptv/iptvc:latest
build:
context: ./iptvc
dockerfile: Dockerfile
command: ["check", "--repeat", "0", "--every", "${CHECKER_WAIT:-60}"]
container_name: iptv-cron
environment:
- CACHE_ENABLED=true
- CACHE_HOST=iptv-keydb
- CACHE_PORT=${KEYDB_PORT:-6379}
- CACHE_USERNAME=${KEYDB_USERNAME}
- CACHE_PASSWORD=${KEYDB_PASSWORD}
- CACHE_DB=${CHECKER_DB:-0}
- CACHE_TTL=${CHECKER_TTL:-1800}
volumes:
- ./playlists/playlists.ini:/app/playlists.ini
- ./playlists/channels.json:/app/channels.json
docs:
<<: *common-attributes
container_name: iptv-docs
image: git.axenov.dev/iptv/iptv-docs:latest
- PHP_IDE_CONFIG=serverName=iptv.local
build:
context: ./docs
dockerfile: Dockerfile
dockerfile: docker/php/${IPTV_ENV}/dockerfile.cron
volumes:
- ./docker/php/${IPTV_ENV}/cron_entrypoint.sh:/entrypoint.sh
- ./docker/php/${IPTV_ENV}/cron_jobs:/etc/cron.d/iptv_jobs
- ./docker/php/${IPTV_ENV}/www.conf:/usr/local/etc/php-fpm.d/www.conf:ro
- ./docker/php/${IPTV_ENV}/php.ini:/usr/local/etc/php/conf.d/php.ini:ro
- ./log/php:/var/log/php:rw
- ./web:/var/www:rw
depends_on:
- keydb
nginx:
<<: *common-attributes
container_name: iptv-nginx
build:
dockerfile: docker/nginx/dockerfile
volumes:
- ./docker/nginx/vhost.conf:/etc/nginx/conf.d/default.conf:ro
- ./log/nginx:/var/log/nginx:rw
- ./web:/var/www:ro
ports:
- 3001:80
- "8080:80"
links:
- php-main
depends_on:
- php-main

1
docker/keydb/dockerfile Normal file
View File

@@ -0,0 +1 @@
FROM eqalpha/keydb:latest AS iptv-keydb

View File

@@ -1,4 +0,0 @@
#!/bin/sh
trap 'echo "Received SIGTERM, saving KeyDB data..."; keydb-cli SAVE; echo "Data saved. Exiting."; exit 0' TERM
echo "[entrypoint] Starting KeyDB..."
exec keydb-server /etc/keydb/keydb.conf "$@"

File diff suppressed because it is too large Load Diff

1
docker/nginx/dockerfile Normal file
View File

@@ -0,0 +1 @@
FROM nginx:latest AS iptv-nginx

View File

@@ -1,14 +1,8 @@
server {
server_name iptv.local;
listen 80;
index index.html index.php;
# access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
add_header Access-Control-Allow-Origin '*';
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers "*";
add_header Access-Control-Allow-Credentials "true";
root /var/www/public;
index index.php;
gzip on;
gzip_vary on;
gzip_proxied any;
@@ -16,31 +10,17 @@ server {
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
location = /docs {
return 301 /docs/;
location ~* \.(jpg|jpeg|gif|css|png|js|ico|html)$ {
access_log off;
expires max;
log_not_found off;
}
location ^~ /docs/ {
proxy_pass http://docs:80/;
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_read_timeout 300;
proxy_connect_timeout 300;
proxy_send_timeout 300;
}
location / {
root /var/www/public;
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
root /var/www/public;
try_files $uri /index.php =404;
fastcgi_pass web:9000;
fastcgi_pass php-main:9000;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
@@ -48,20 +28,6 @@ server {
fastcgi_hide_header X-Powered-By;
fastcgi_read_timeout 300;
proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_send_timeout 300;
include fastcgi_params;
location ~* \.(jpg|jpeg|gif|css|png|ttf|woff|svg|js|ico)$ {
access_log off;
expires max;
log_not_found off;
}
}
location ~* \.(jpg|jpeg|gif|css|png|ttf|woff|svg|js|ico)$ {
root /var/www/public;
access_log off;
expires max;
log_not_found off;
}
}

View File

@@ -0,0 +1,17 @@
#!/bin/bash
echo "Current pwd: $(pwd)"
for file in "/etc/cron.d/iptv_jobs" "/var/www/iptv-cli"; do
if [ ! -f "$file" ]; then echo "Not found: $file" && exit 1; fi
done;
echo "Importing crontab /etc/cron.d/iptv_jobs:"
echo "======================="
cat /etc/cron.d/iptv_jobs
echo "======================="
crontab -n /etc/cron.d/iptv_jobs || exit 2
echo "Running cron with /etc/cron.d/iptv_jobs"
crontab /etc/cron.d/iptv_jobs
touch /var/log/cron.log
cron -L 15 && tail -fn 1 /var/log/cron.log

5
docker/php/dev/cron_jobs Normal file
View File

@@ -0,0 +1,5 @@
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
PHP_IDE_CONFIG=serverName=iptv.local
* * * * * /var/www/iptv-cli check:ini --count 5 --order random

View File

@@ -0,0 +1,44 @@
FROM php:8.4-fpm AS iptv-img-base
RUN apt update && \
apt upgrade -y && \
apt install -y \
git \
unzip \
7zip \
cron \
zlib1g-dev \
imagemagick \
libpng-dev \
libjpeg-dev
# https://pecl.php.net/package/xdebug
# https://pecl.php.net/package/redis
RUN pecl channel-update pecl.php.net && \
pecl install \
xdebug-3.4.1 \
redis-6.1.0
RUN docker-php-ext-enable redis && \
docker-php-ext-configure gd --with-jpeg && \
docker-php-ext-install gd && \
docker-php-ext-configure pcntl --enable-pcntl && \
docker-php-ext-install pcntl
RUN mkdir -p /var/run/php && \
mkdir -p /var/log/php && \
chmod -R 777 /var/log/php
COPY --from=composer /usr/bin/composer /usr/local/bin/composer
RUN git config --global --add safe.directory /var/www
EXPOSE 9000
WORKDIR /var/www
CMD ["composer", "install"]
################################################################
FROM iptv-img-base AS iptv-img-cron
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,44 @@
FROM php:8.4-fpm AS iptv-img-base
RUN apt update && \
apt upgrade -y && \
apt install -y \
git \
unzip \
7zip \
cron \
zlib1g-dev \
imagemagick \
libpng-dev \
libjpeg-dev
# https://pecl.php.net/package/xdebug
# https://pecl.php.net/package/redis
RUN pecl channel-update pecl.php.net && \
pecl install \
xdebug-3.4.1 \
redis-6.1.0
RUN docker-php-ext-enable xdebug redis && \
docker-php-ext-configure gd --with-jpeg && \
docker-php-ext-install gd && \
docker-php-ext-configure pcntl --enable-pcntl && \
docker-php-ext-install pcntl
RUN mkdir -p /var/run/php && \
mkdir -p /var/log/php && \
chmod -R 777 /var/log/php
COPY --from=composer /usr/bin/composer /usr/local/bin/composer
RUN git config --global --add safe.directory /var/www
EXPOSE 9000
WORKDIR /var/www
CMD ["composer", "install"]
################################################################
FROM iptv-img-base AS iptv-img-main
ENTRYPOINT ["php-fpm", "--nodaemonize"]

26
docker/php/dev/php.ini Normal file
View File

@@ -0,0 +1,26 @@
[PHP]
error_reporting = E_ALL & ~E_NOTICE & ~E_DEPRECATED
expose_php = Off
file_uploads = Off
max_execution_time=-1
memory_limit = 512M
[opcache]
opcache.enable = 1
opcache.enable_cli = 1
opcache.memory_consumption = 128
opcache.max_accelerated_files = 30000
opcache.revalidate_freq = 0
opcache.jit_buffer_size = 64M
opcache.jit = tracing
[xdebug]
; https://xdebug.org/docs/all_settings
zend_extension = xdebug.so
xdebug.mode = debug
xdebug.start_with_request = yes
xdebug.trigger_value = go
xdebug.client_host = host.docker.internal
xdebug.REQUEST = *
xdebug.SESSION = *
xdebug.SERVER = *

22
docker/php/dev/www.conf Normal file
View File

@@ -0,0 +1,22 @@
[www]
user = www-data
group = www-data
listen = 127.0.0.1:9000
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 50
pm.status_path = /status
ping.path = /ping
ping.response = pong
access.log = /var/www/$pool.access.log
;access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{milli}d %{kilo}M %C%%"
; chroot = /var/www
; chdir = /var/www
php_flag[display_errors] = on
php_admin_value[error_log] = /var/www/$pool.error.log
php_admin_flag[log_errors] = on
php_admin_value[memory_limit] = 512M
php_admin_value[error_reporting] = E_ALL & ~E_NOTICE & ~E_DEPRECATED

View File

@@ -0,0 +1,21 @@
FROM php:8.3-fpm
RUN apt update && \
apt upgrade -y && \
apt install -y git
# https://pecl.php.net/package/redis
RUN pecl channel-update pecl.php.net && \
pecl install redis && \
docker-php-ext-enable redis && \
mkdir -p /var/log/php && \
chmod -R 777 /var/log/php && \
git config --global --add safe.directory /var/www
COPY --from=composer /usr/bin/composer /usr/local/bin/composer
USER www-data
EXPOSE 9000
WORKDIR /var/www
CMD composer install --no-dev --optimize-autoloader
ENTRYPOINT php-fpm

16
docker/php/prod/php.ini Normal file
View File

@@ -0,0 +1,16 @@
[PHP]
error_reporting = E_ALL
expose_php = Off
file_uploads = Off
memory_limit = 512M
; upload_max_filesize=10M
; post_max_size=10M
[opcache]
opcache.enable = 1
opcache.enable_cli = 1
opcache.memory_consumption = 128
opcache.max_accelerated_files = 30000
opcache.revalidate_freq = 0
opcache.jit_buffer_size = 64M
opcache.jit = tracing

21
docker/php/prod/www.conf Normal file
View File

@@ -0,0 +1,21 @@
[www]
user = www-data
group = www-data
listen = 127.0.0.1:9000
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
pm.max_requests = 50
pm.status_path = /status
ping.path = /ping
ping.response = pong
access.log = /var/log/php/$pool.access.log
;access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{milli}d %{kilo}M %C%%"
; chroot = /var/www
; chdir = /var/www
php_flag[display_errors] = on
php_admin_value[error_log] = /var/log/php/$pool.error.log
php_admin_flag[log_errors] = on
php_admin_value[memory_limit] = 512M

622
iptv
View File

@@ -21,10 +21,10 @@ else
ROOT_PATH="$(pwd)"
fi
IPTV_PROJECTS=("web" "playlists")
IPTV_GITEA_URL_SSH="git@git.axenov.dev:IPTV"
IPTV_GITEA_URL_HTTPS="https://git.axenov.dev/IPTV"
IPTV_DOCKER_URL_SSH="$IPTV_GITEA_URL_SSH/iptv-docker.git"
IPTV_DOCKER_URL_SSH="$IPTV_GITEA_URL_SSH/docker.git"
IPTV_DOXYGEN_OUTPUT="$ROOT_PATH/docs/doxygen"
for sig in SIGHUP SIGINT SIGQUIT SIGABRT SIGKILL SIGTERM SIGTSTP; do
# https://faculty.cs.niu.edu/~hutchins/csci480/signals.htm
@@ -154,6 +154,15 @@ grep_match() {
printf "%s" "$1" | grep -qE "$2" >/dev/null 2>&1
}
# Очищает строку аргументов от тех, которые не следует передавать в docker
clear_flags() {
args=${*/--full/}
args=${args/-f/}
args=${args/--root/}
args=${args/-r/}
echo "$args"
}
# Возвращает название текущей ОС
get_os() {
case "$(uname -s)" in
@@ -288,6 +297,12 @@ git.is_repo() {
is_dir "$1/" && is_dir "$1/.git/"
}
# Требует, чтобы в директории был инициализирован репозиторий
git.require_repo() {
require git
git.is_repo "$1" || die "'$1' is not git repository!" 10
}
# Клонирует репозиторий
git.clone() {
require git
@@ -296,6 +311,74 @@ git.clone() {
$cmd
}
# Клонирует репозиторий быстро
git.clone_quick() {
require git
git.clone --depth=1 --single-branch "$@"
}
# Изменяет значение конфигурацию репозитория по ключу
git.cfg_set() {
require git
cmd="git config --replace-all $1 $2"
debug "Команда: $cmd"
$cmd
}
# Получает значение конфигурацию репозитория по ключу
git.cfg_get() {
require git
cmd="git config --get $1"
debug "Команда: $cmd"
$cmd
}
# Устанавливает имя $1 и email $2 пользователя git в конфигурации репозитория
git.set_user() {
require git
local name="$1"
local email="$2"
git.cfg_set "user.name" "$name"
git.cfg_set "user.email" "$email"
success "Установлен пользователь git: '$name <$email>'"
}
# Получает изменения из удалённого репозитория и сливает их в текущую ветку
# shellcheck disable=SC2120
git.pull() {
require git
git pull "$@" 2>&1
}
# Получает изменения из удалённого репозитория и сливает их в текущую ветку
git.fetch() {
require git
if [ "$1" ]; then
if git.remote_branch_exists "origin/$1"; then
git fetch origin "refs/heads/$1:refs/remotes/origin/$1" --progress --prune --quiet 2>&1 \
|| die "Could not fetch $1 from origin" 12
else
warn "Tried to fetch branch 'origin/$1' but it does not exist."
fi
else
git fetch origin --progress --prune --quiet 2>&1 || exit 12
fi
}
# Переключает HEAD
git.checkout() {
require git
git checkout "$@" 2>&1
}
# Возвращает имя текущей ветки
git.current_branch() {
require git
git branch --show-current
}
########################################################
# Функции для работы с docker
########################################################
@@ -338,7 +421,6 @@ docker.build_base_images() {
# done
subtitle "Построение образов"
docker.compose build
[ ! -d web/cache/views ] && mkdir -p web/cache/views
success "Базовые образы построены"
}
@@ -363,12 +445,12 @@ docker.exec_www() {
# Возвращает ssh-адрес к репозиторию проекта
project_url_ssh() {
echo "$IPTV_GITEA_URL_SSH/$1.git"
echo "$IPTV_GITEA_URL_SSH/${IPTV_PROJECTS[$1]}.git"
}
# Возвращает https-адрес к репозиторию проекта
project_url_https() {
echo "$IPTV_GITEA_URL_HTTPS/$1"
echo "$IPTV_GITEA_URL_HTTPS/${IPTV_PROJECTS[$1]}"
}
# Копирует .env.example в .env, если возможно
@@ -379,6 +461,20 @@ prepare_dot_env() {
fi
}
# Устанавливает в файле .env для переменной $1 значение $2
set_env_value() {
local key="$1"
local value="$2"
debug "Установка '$key=$value' в файле .env"
if [[ "$(get_os)" = "Macos" ]]; then
sed -Ei '' "s|$key=.+|$key=$value|g" .env
else
sed -Ei "s|$key=.+|$key=$value|g" .env
fi
}
# Клонирует проект
project_clone() {
local repo_url="$1"; shift
@@ -415,9 +511,9 @@ find_service_compose() {
svc="$1"
[ -z "$svc" ] && die "неизвестный сервис" 2
for known in $(docker.compose config --services); do
for known in $(docker.compose --profile full config --services); do
if [ "$known" = "$svc" ]; then
debug "Сервис '$known' найден в композе"
debug "Сервис '$svc' найден в композе"
echo "$known"
exit
fi
@@ -433,13 +529,20 @@ find_services_compose() {
[ "$*" ] && for svc in "$@"; do
grep_match "$svc" "^--?.*" && continue
svc="$(find_service_compose "${svc/iptv-/}")"
var_dump svc
svc="$(find_service_compose "$svc")"
services="$services $svc"
done
trim "$services"
}
# Возвращает состояние контейнера
container_state() {
state=$(docker.inspect -f '{{.State.Status}}' "$1")
echo "${state//\'/}"
}
########################################################
# Главные функции обработки команд
########################################################
@@ -464,21 +567,22 @@ init() {
prepare_dot_env
local counter=1
local repo_count=${#IPTV_PROJECTS[@]}
for repo_name in "${IPTV_PROJECTS[@]}"; do
local project_repo_path="$docker_repo_path/$repo_name"
subtitle "[$counter/$repo_count] Подготовка репозитория ${FBOLD}$repo_name${FRESET}..."
local projects_count=${#IPTV_PROJECTS[@]}
for project_name in "${!IPTV_PROJECTS[@]}"; do
local project_repo_path="$docker_repo_path/$project_name"
subtitle "[$counter/$projects_count] Подготовка проекта ${FBOLD}$project_name${FRESET}..."
local project_repo_url=$(project_url_https "$repo_name")
local project_repo_url=$(project_url_ssh "$project_name")
debug "Известная ссылка на репозиторий: $project_repo_url"
project_clone "$project_repo_url" "$project_repo_path"
cd "$project_repo_path"
# git.checkout dev
prepare_dot_env
cd - >/dev/null
success "Репозиторий $repo_name готов"
success "Проект $project_name готов"
counter=$((counter+1))
done
@@ -495,11 +599,12 @@ up() {
process_help_arg
subtitle "Создание и запуск контейнеров"
argl profiles 0 profiles
local services=''
[ "$*" ] && services="$(find_services_compose "$@")"
[ ! -d web/cache/views ] && mkdir -p web/cache/views
docker.compose up "$services" --build --detach --remove-orphans && \
COMPOSE_PROFILES="$profiles" docker.compose up "$services" --build --detach --remove-orphans && \
success 'Среда запущена успешно'
}
@@ -522,10 +627,13 @@ down() {
process_help_arg
subtitle "Остановка и удаление контейнеров"
argl profiles 0 profiles
[[ -z "$profiles" ]] && profiles="full"
local services=''
[ "$*" ] && services="$(find_services_compose "$@")"
docker.compose down "$services" --remove-orphans && \
COMPOSE_PROFILES="$profiles" docker.compose down "$services" --remove-orphans && \
success 'Среда остановлена успешно'
}
@@ -551,7 +659,7 @@ rebuild() {
is_full=$(arg full 1)
[ "$is_full" = 0 ] && is_full=$(argl full 1)
[ -n "$*" ] && down "$@" || down
[ -n "$*" ] && down "$@"
[ "$is_full" = 1 ] && docker.build_base_images
up "$@"
@@ -571,11 +679,150 @@ purge() {
down
docker rmi "$(docker image list | grep iptv- | awk '{print $3}')"
docker rmi iptv/doxygen
docker system prune --all --force
success 'Образы удалены успешно'
}
# Обновляет все репозитории
update() {
process_help_arg
local docker_repo_path=$ROOT_PATH
local basename=$(basename "$docker_repo_path")
if [ ! "$basename" = "iptv-docker" ]; then
docker_repo_path="$docker_repo_path/iptv-docker"
fi
subtitle "Обновление локальной среды..."
cd "$docker_repo_path"
need_pull=$(arg pull 1)
[[ "$need_pull" = 0 ]] && need_pull=$(argl pull 1)
[[ "$need_pull" = 1 ]] && git.pull || git.fetch
local counter=1
local projects_count=${#IPTV_PROJECTS[@]}
for project_name in "${!IPTV_PROJECTS[@]}"; do
local project_repo_path="$docker_repo_path/$project_name"
subtitle "[$counter/$projects_count] Обновление проекта ${FBOLD}$project_name${FRESET}..."
cd "$project_repo_path"
#TODO при грязном дереве + $need_pull спрашивать (стеш, сброс), учитывать перед 'cd -' и поддержать аргументы
git.checkout dev
[ "$need_pull" = 1 ] && git.pull && git.fetch
cd - >/dev/null
success "Проект $project_name обновлён"
counter=$((counter+1))
done
success "Локальная среда обновлена"
}
# Генерирует флеймграф
flame() {
process_help_arg
require zcat git
arg trace 0 trace_file_path
[ -z "$trace_file_path" ] && argl trace 0 trace_file_path
[ -z "$trace_file_path" ] && die 'Файл трейса не указан'
trace_file_path=$(abspath "$trace_file_path")
debug "Файл трейса: $trace_file_path"
is_file "$trace_file_path" || die 'Файл трейса не найден'
svg_file_path=${trace_file_path/xt.gz/svg}
debug "Файл флеймграфа: $svg_file_path"
flame_repo_path="$ROOT_PATH/.tools/FlameGraph"
if is_dir "$flame_repo_path"; then
need_update=$(arg update 1)
[ "$need_update" = 0 ] && need_update=$(argl update 1)
if [ "$need_update" = 1 ]; then
inform 'Подготовка генератора...'
cd "$flame_repo_path"
git.pull
cd - >/dev/null
fi
else
inform 'Подготовка генератора...'
# https://derickrethans.nl/flamboyant-flamegraphs.html
# https://github.com/interstellar-space/FlameGraph
git.clone_quick https://github.com/brendangregg/FlameGraph.git "$flame_repo_path"
fi
inform "Генерация флеймграфа (может занять некоторое время)..."
zcat "$trace_file_path" | "$flame_repo_path/flamegraph.pl" > "$svg_file_path"
exit_code=$?
if [[ $exit_code -gt 0 ]]; then
exit $exit_code
fi
success "Флеймграф сохранён. Открой файл в браузере:"
print "$svg_file_path"
}
# Исправляет некоторые возможные проблемы
fix() {
process_help_arg
require docker
subtitle "Попытка исправления построения образов"
print "Список проблем, которые можно попробовать решить:"
print " 1. ошибки при построении образов"
print " 2. ошибки прав на директории vendor/bars/*"
print " 3. ошибки записи в директорию"
ask "Выбери свою проблему (Enter - выход)" choose
# shellcheck disable=SC2154
case $choose in
1)
purge
docker.build_base_images
up
;;
2)
ask "Введи имя сервиса" svc
svc_correct=$(find_service_hardcoded "$svc")
[ -z "$svc_correct" ] && exit
dirs=("$(docker.exec "$svc_correct" "find /var/www/vendor/bars -mindepth 1 -maxdepth 1 -type d")")
for dir in "${dirs[@]}"; do
inform "Исправление $dir"
docker.exec_www "$svc_correct" git config --global --add safe.directory "$dir"
done
;;
3)
ask "Введи имя сервиса" svc
svc_correct=$(find_service_hardcoded "$svc")
[ -z "$svc_correct" ] && exit
# current=$(docker.exec "$svc_correct" sh -c 'pwd')
ask "Введи директорию (Enter - выбор текущей /var/www)" dir
[ -z "$dir" ] && dir='/var/www'
if [[ $IPTV_DEBUG -gt 0 ]]; then
docker.exec "$svc_correct" find "$dir" -type d -print -exec chmod a+w {} +
docker.exec "$svc_correct" find "$dir" -type f -print -exec chmod a+w {} +
else
docker.exec "$svc_correct" find "$dir" -type d -exec chmod a+w {} +
docker.exec "$svc_correct" find "$dir" -type f -exec chmod a+w {} +
fi
;;
esac
success "Готово"
}
# Выполняет команду в контейнере
exec() {
process_help_arg
@@ -583,7 +830,7 @@ exec() {
svc="$1"
regex_match "$svc" "--?r(oot)?" && { as_root=1; shift; svc="$1"; }
svc_correct="iptv-$(find_service_compose "$svc")"
svc_correct="$(find_service_compose "$svc")"
command=("${@:2}")
regex_match "${command[0]}" "--?r(oot)?" && { as_root=1; unset "command[0]"; }
@@ -602,7 +849,8 @@ logs() {
process_help_arg
[ -z "$1" ] && die "не указан сервис" 8
svc=$(find_service_compose "$1")
svc=$(find_service_hardcoded "$1")
[ -z "$svc" ] && svc=$(find_service_compose "$1")
[ -z "$svc" ] && die "неизвестный сервис" 3
subtitle "Логи сервиса $svc"
@@ -613,6 +861,113 @@ logs() {
$cmd
}
# Выводит список профилей
profiles() {
process_help_arg
subtitle "Список доступных профилей"
profiles=("$(docker.compose config --profiles | sort)")
IFS=$'\n' sorted=("${profiles[@]}")
for profile in "${sorted[@]}"; do
print " $profile"
done
echo
inform "Просмотра списка сервисов в профиле используй './iptv services ПРОФИЛЬ'"
}
# Выводит список сервисов профиля
services() {
process_help_arg
[ -z "$1" ] && die "не указан профиль" 8
subtitle "Список сервисов в профиле: $1"
services=$(docker.compose --profile "$1" config --services | sort)
IFS=$'\n' sorted=("${services[@]}")
for service in "${sorted[@]}"; do
print " $service"
done
echo
inform "Для просмотра ПО сервиса используй './iptv info СЕРВИС'"
inform "Для просмотра состояния сервиса используй './iptv status СЕРВИС'"
inform "Для выполнения команды в сервисе используй './iptv exec СЕРВИС'"
}
# Выводит информацию о ПО в контейнере
info() {
process_help_arg
svc=$(find_service_hardcoded "$1")
subtitle "Состояние сервиса $svc"
#TODO как-то определять состав ПО, от этого уже выводить что-то дальше
docker.exec "$svc" date
subtitle "Переменные среды:"
docker.exec_www "$svc" env && echo
subtitle "Версии ПО:"
docker.exec "$svc" cat /etc/os-release && echo
docker.exec "$svc" git --version && echo
docker.exec "$svc" php -v && echo
docker.exec "$svc" composer --version && echo
docker.exec "$svc" php artisan --version && echo
docker.exec "$svc" go version && echo
docker.exec "$svc" go env
subtitle "Содержимое \$_SERVER (php):"
docker.exec_www "$svc" php -i | grep _SERVER
subtitle "Загруженные расширения (php):"
docker.exec_www "$svc" php -m
}
# Выводит состояние сервиса
status() {
process_help_arg
[ -z "$1" ] && die "не указан сервис" 8
svc=$(find_service_hardcoded "$1")
[ -z "$svc" ] && svc=$(find_service_compose "$1")
[ -z "$svc" ] && die "неизвестный сервис" 3
subtitle "Состояние сервиса $svc"
inform "Код:"
# shellcheck disable=SC2015
git.is_repo "$ROOT_PATH/$svc/app/" \
&& print " Репозиторий: +" \
|| print " Репозиторий: -"
if is_dir "$ROOT_PATH/$svc/app/"; then
cd "$ROOT_PATH/$svc/app/" 2>/dev/null
print " Ветка: $(git.current_branch)"
cd - >/dev/null
fi
# shellcheck disable=SC2015
is_file "$ROOT_PATH/$svc/app/.env" && \
print " Файл app/.env: +" || \
print " Файл app/.env: -"
inform "Контейнер:"
ips=$(docker.inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$svc")
print " IP: ${ips:--}"
print " State: $(container_state "$svc")"
health=$(docker.inspect -f '{{.State.Health.Status}}' "$svc")
health=${health//\'/}
print " Health: ${health:--}"
print " Running: $(docker.inspect -f '{{.State.Running}}' "$svc")"
print " Paused: $(docker.inspect -f '{{.State.Paused}}' "$svc")"
print " Restarting: $(docker.inspect -f '{{.State.Restarting}}' "$svc")"
print " OOMKilled: $(docker.inspect -f '{{.State.OOMKilled}}' "$svc")"
print " Dead: $(docker.inspect -f '{{.State.Dead}}' "$svc")"
error=$(docker.inspect -f '{{.State.Error}}' "$svc")
print " Error: ${error:--}"
}
# Выводит статистику потребления ресурсов контейнерами
stats() {
subtitle "Состояние контейнеров"
@@ -620,6 +975,51 @@ stats() {
docker.compose stats "$@"
}
# Генерирует документацию по коду сервиса
doxygen() {
process_help_arg
require docker
if [[ $(docker image ls | grep -c iptv/doxygen) -eq 0 ]]; then
inform "Подготовка doxygen..."
docker build -t iptv/doxygen "$ROOT_PATH/.tools/doxygen"
fi
svc=$(find_service_hardcoded "$1")
[ -z "$svc" ] && die "неизвестный сервис" 3
subtitle "Генерация документации для $svc"
[[ $IPTV_DEBUG -gt 0 ]] && args="" || args="-q"
doxyfile="$ROOT_PATH/.tools/doxygen/$svc.Doxyfile"
is_file "$doxyfile" || die "Не найден файл $doxyfile" 8
input_dir="$ROOT_PATH/$svc"
is_dir "$input_dir" || die "Не найдена директория $input_dir" 9
output_dir="$IPTV_DOXYGEN_OUTPUT/$svc"
is_dir "$output_dir" || mkdir -p "$output_dir"
cmd="docker run --rm -it -v $input_dir:/data -v $output_dir:/output -v $doxyfile:/data/Doxyfile iptv/doxygen $args"
debug "Команда: $cmd"
# $cmd
rm -f "$input_dir/Doxyfile"
doc_path=$(basename "$IPTV_DOXYGEN_OUTPUT")
html_path="$IPTV_DOXYGEN_OUTPUT/../doxygen.html"
html="<!DOCTYPE html><html><head><meta charset='UTF-8'><style>*{font-family:'monospace'}h3{margin-bottom:0}</style><title>LisUP Doxygen</title></head>"
html="$html<body><h1>Документация к коду сервисов</h1><p><b>Для справки по генерации документации выполни в терминале './iptv doxygen --help'</b></p><ul>"
for html_dir in "$IPTV_DOXYGEN_OUTPUT"/*; do
doc_svc=$(basename "$html_dir")
mtime=$(date -d"$(stat -c '%y' "$html_dir")" +'%d.%m.%Y %H:%M')
html="$html<li><h3><a href='./$doc_path/$doc_svc/html/index.html'>$doc_svc</a></h3><b>Обновлено:</b> $mtime<br><b>Команда:</b> ./iptv doxygen $doc_svc</li>"
done
echo "$html</ul></body></html>" > "$html_path"
echo
success "Готово. Открой в браузере файл $(realpath "$html_path")"
}
########################################################
# Команды справки
########################################################
@@ -707,11 +1107,15 @@ help.up() {
print " ./iptv up [АРГУМЕНТЫ] [СЕРВИСЫ...]"
print
print "Доступные АРГУМЕНТЫ:"
print " -f|--full - перестроить также и базовые образы (если опущено, будут"
print " построены только образы сервисов)"
print " --profiles - использовать указанные профили (через запятую)"
print " -h|--help - вывести это сообщение и выйти"
print
print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним."
print
print "СЕРВИСЫ можно передавать без префикса 'iptv-'"
# print "СЕРВИСЫ можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.start() {
@@ -722,11 +1126,13 @@ help.start() {
print " ./iptv start [АРГУМЕНТЫ] [СЕРВИСЫ...]"
print
print "Доступные АРГУМЕНТЫ:"
print " --profiles - использовать указанные профили (через запятую)"
print " -h|--help - вывести это сообщение и выйти"
print
print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним."
print
print "СЕРВИСЫ можно передавать без префикса 'iptv-'"
# print "СЕРВИСЫ можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.down() {
@@ -737,11 +1143,13 @@ help.down() {
print " ./iptv down [АРГУМЕНТЫ] [СЕРВИСЫ...]"
print
print "Доступные АРГУМЕНТЫ:"
print " --profiles - использовать указанные профили (через запятую)"
print " -h|--help - вывести это сообщение и выйти"
print
print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним."
print
print "СЕРВИСЫ можно передавать без префикса 'iptv-'"
# print "СЕРВИСЫ можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.stop() {
@@ -752,11 +1160,13 @@ help.stop() {
print " ./iptv stop [АРГУМЕНТЫ] [СЕРВИСЫ...]"
print
print "Доступные АРГУМЕНТЫ:"
print " --profiles - использовать указанные профили (через запятую)"
print " -h|--help - вывести это сообщение и выйти"
print
print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним."
print
print "СЕРВИСЫ можно передавать без префикса 'iptv-'"
# print "СЕРВИСЫ можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.rebuild() {
@@ -767,11 +1177,15 @@ help.rebuild() {
print " ./iptv rebuild [АРГУМЕНТЫ] [СЕРВИСЫ...]"
print
print "Доступные АРГУМЕНТЫ:"
print " -f|--full - перестроить базовые образы (если опущено, будут"
print " построены только образы сервисов)"
print " --profiles - использовать указанные профили (через запятую)"
print " -h|--help - вывести это сообщение и выйти"
print
print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним."
print
print "СЕРВИСЫ можно передавать без префикса 'iptv-'"
# print "СЕРВИСЫ можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.restart() {
@@ -786,7 +1200,8 @@ help.restart() {
print
print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним."
print
print "СЕРВИСЫ можно передавать без префикса 'iptv-'"
# print "СЕРВИСЫ можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.purge() {
@@ -800,6 +1215,80 @@ help.purge() {
print " -h|--help - вывести это сообщение и выйти"
}
help.update() {
print "КОМАНДА: update"
print "Обновляет окружение разработки, используя 'git fetch'."
print
print "Использование:"
print " ./iptv update [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -p|--pull - использовать 'git pull' вместо 'git fetch'"
print " -h|--help - вывести это сообщение и выйти"
print
print "Перед выполнением, необходимо обеспечить чистую рабочую директорию репозитория."
}
help.setenv() {
print "КОМАНДА: setenv"
print "Переключает среду в каждом сервисе окружения. Устанавливает 'APP_ENV'"
print "в файле .env сервиса и переключает git HEAD на тег, указанный в iptv-docker/.env."
print
print "Использование:"
print " ./iptv setenv СРЕДА [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
print
print "Перед выполнением, необходимо обеспечить чистую рабочие директории репозиториев."
}
help.flame() {
print "КОМАНДА: flame"
print "Выполняет построение флеймграфа по снимку трейса xdebug."
print
print "Использование:"
print " ./iptv flame АРГУМЕНТЫ"
print
print "Доступные АРГУМЕНТЫ:"
print " -t|--trace ПУТЬ - путь до локального файла трейса"
print " -u|--update - сначала обновить генератор"
print " -h|--help - вывести это сообщение и выйти"
}
help.fix() {
print "КОМАНДА: fix"
print "В интерактивном режиме пытается исправить локальную среду в случае ошибок."
print
print "Использование:"
print " ./iptv fix [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
}
help.exec() {
print "КОМАНДА: exec"
print "Выполняет произвольную команду внутри контейнера."
print
print "Использование:"
print " ./iptv exec СЕРВИС [АРГУМЕНТЫ] ВНУТ-КОМАНДА..."
print
print "Доступные АРГУМЕНТЫ:"
print " -r|--root - выполнить ВНУТ-КОМАНДУ от имени root (если опущено, будет выполнена"
print " от имени www-data)"
print " -h|--help - вывести это сообщение и выйти"
print
print "Примеры:"
print " ./iptv exec arm composer i"
print " ./iptv exec core -r chmod 0777 storage/logs"
print
print "ВНУТ-КОМАНДА, которая должна выполниться внутри контейнера, может иметь свои аргументы."
print
# print "СЕРВИС можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.logs() {
print "КОМАНДА: logs"
print "Выводит логи указанного СЕРВИСА."
@@ -812,7 +1301,60 @@ help.logs() {
print
print "Поддерживаются АРГУМЕНТЫ 'docker logs'."
print
print "СЕРВИС можно передавать без префикса 'iptv-'"
# print "СЕРВИС можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.profiles() {
print "КОМАНДА: profiles"
print "Выводит список возможных профилей для запуска окружения."
print
print "Использование:"
print " ./iptv profiles [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
}
help.services() {
print "КОМАНДА: services"
print "Выводит список сервисов в указанном ПРОФИЛЕ."
print
print "Использование:"
print " ./iptv services ПРОФИЛЬ [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
print
print "Полный список ПРОФИЛЕЙ можно узнать командой './iptv profiles'"
}
help.info() {
print "КОМАНДА: info"
print 'Выводит дату, версии ПО, переменные среды и пр. из контейнера указанного СЕРВИСА.'
print
print "Использование:"
print " ./iptv info СЕРВИС [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
print
# print "СЕРВИС можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.status() {
print "КОМАНДА: status"
print "Выводит состояние СЕРВИСА и его репозитория (при наличии)."
print
print "Использование:"
print " ./iptv status СЕРВИС [АРГУМЕНТЫ]"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
print
# print "СЕРВИС можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
help.stats() {
@@ -829,6 +1371,21 @@ help.stats() {
print "Рекомендуется использовать вместе с '--no-stream'."
}
help.doxygen() {
print "КОМАНДА: doxygen"
print "Вызывает doxygen для генерации дкоументации к коду указанного СЕРВИСА и индексной"
print "страницы $IPTV_DOXYGEN_OUTPUT/doxygen.html"
print
print "Использование:"
print " ./iptv doxygen СЕРВИС"
print
print "Доступные АРГУМЕНТЫ:"
print " -h|--help - вывести это сообщение и выйти"
print
# print "СЕРВИС можно передавать без приставки 'lis-'"
print "Полный список СЕРВИСОВ можно узнать командой './iptv services full'"
}
########################################################
# Точка входа
########################################################
@@ -844,9 +1401,18 @@ case "$COMMAND" in
stop ) stop "$@" ;;
r|rebuild ) rebuild "$@" ;;
restart ) restart "$@" ;;
exec ) exec "$@" ;;
purge ) purge ;;
update ) update ;;
s|setenv ) setenv "$@" ;;
f|flame ) flame "$@" ;;
fix ) fix ;;
exec ) exec "$@" ;;
logs ) logs "$@" ;;
profiles ) profiles ;;
services ) services "$1" ;;
info ) info "$1" ;;
status ) status "$1" ;;
stats ) stats "$@" ;;
doxygen ) doxygen "$@" ;;
* ) die "неизвестная команда. Смотри './iptv help' для справки." ;;
esac

0
log/redis/.gitkeep Normal file
View File