diff --git a/.env.example b/.env.example index 2c2df1d..f5c2014 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,13 @@ IPTV_ENV=dev -REDIS_UID=1000 -REDIS_GID=1000 -REDIS_PORT=6379 +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 diff --git a/.gitignore b/.gitignore index 3f72ab7..6291fec 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,16 @@ -/.idea -/.vscode -downloaded/ -/svc-* -/tools -/tmp +/.idea/ +/.vscode/ +/iptvc/ +/web/ +/playlists/ +/tools/ +/.profile/ +/tmp/ -*.log .env +*.log *.m3u -*.m3u.* *.m3u8 -*.m3u8.* *.rdb !/**/.gitkeep diff --git a/README.md b/README.md index 7d61b09..2323792 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,54 @@ # Инфраструктурный слой проекта iptv.axenov.dev -> **Адрес**: https://iptv.axenov.dev -> **FAQ**: https://iptv.axenov.dev/faq -> **Исходный код**: https://git.axenov.dev/IPTV +Docker-окружение для работы проекта iptv.axenov.dev. -Содержит docker-окружение для запуска проекта iptv.axenov.dev. +> **Веб-сайт:** [iptv.axenov.dev](https://iptv.axenov.dev) +> **Зеркало:** [m3u.su](https://m3u.su) +> Исходный код: [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) +> Дополнительные сведения: [git.axenov.dev/IPTV/.profile](https://git.axenov.dev/IPTV/.profile) ## Использованный стек * [docker compose](https://docs.docker.com/compose/) -* [php8.3-fpm](https://www.php.net/releases/8.3/ru.php) +* [php8.4-fpm](https://www.php.net/releases/8.4/ru.php) * [nginx](https://nginx.org/ru/) * [keydb](https://docs.keydb.dev/docs/) +* [iptvc](https://git.axenov.dev/IPTV/iptvc) * bash ## Установка и настройка ``` -git clone https://git.axenov.dev/IPTV/docker.git iptv -cp .env.example .env -git clone https://git.axenov.dev/IPTV/svc-main.git -cp svc-main/.env.example svc-main/.env -docker exec -it iptv-php composer i -docker compose up -d --build +wget -O - https://git.axenov.dev/IPTV/iptv-docker/raw/branch/master/iptv | bash -s - init ``` -### Описание переменных окружения +## Cкрипт [`iptv`](./iptv) + +Это инструмент, который позволяет быстро управлять локальной средой `lis-docker`: +* инициализировать с нуля, как в примере выше; +* управлять образами и контейнерами среды. + +> Управление средой не всегда удобно через команды git и docker, поэтому рекомендуется использовать `./iptv`. + +Набери `./iptv help` для справки по использованию. + +При доработке используй [линтер](https://www.shellcheck.net): `shellcheck -s bash iptv` + +## Описание переменных окружения * `IPTV_ENV` -- окружение для развёртывания: это имена директорий и/или префиксы имён конфигов, которые будут проброшены в контейнеры; -* `REDIS_UID`, `REDIS_GID` -- ID поьзователя/группы для разрешения владельца файлов и директорий keydb; -* `REDIS_PORT` -- порт keydb, который будет проброшен на хост. +* `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-сертификаты или др. diff --git a/compose.yml b/compose.yml index 2e4e341..f47276f 100644 --- a/compose.yml +++ b/compose.yml @@ -20,26 +20,53 @@ services: <<: *common-attributes container_name: iptv-keydb image: eqalpha/keydb:latest - user: "${REDIS_UID}:${REDIS_GID}" + user: "${KEYDB_UID}:${KEYDB_GID}" volumes: - ./docker/keydb/keydb.conf:/etc/keydb/keydb.conf - ./docker/keydb/data/:/data:rw - ./log/keydb:/var/log/keydb/:rw ports: - - "${REDIS_PORT:-6379}:6379" + - "${KEYDB_PORT:-6379}:6379" - php: + web: <<: *common-attributes - container_name: iptv-php + container_name: iptv-web + build: + dockerfile: dockerfile.web.${IPTV_ENV} environment: - PHP_IDE_CONFIG=serverName=iptv.local - build: - dockerfile: docker/php/${IPTV_ENV}/dockerfile volumes: - ./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 + - ./playlists/playlists.ini:/var/www/config/playlists.ini + # - ./playlists/channels.json:/var/www/config/channels.json - ./log/php:/var/log/php:rw - - ./svc-main:/var/www:rw + - ./web:/var/www:rw + depends_on: + - keydb + + checker: + <<: *common-attributes + container_name: iptv-checker + build: + dockerfile: ./dockerfile.checker + environment: + - CACHE_ENABLED=true + # - CACHE_HOST=localhost + - 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} + - CHECKER_WAIT=${CHECKER_WAIT:-60} + - CHECKER_INIFILE=${CHECKER_INIFILE:-/app/playlists.ini} + - CHECKER_TAGFILE=${CHECKER_TAGFILE:-/app/channels.json} + volumes: + - ./docker/checker/entrypoint.sh:/entrypoint.sh + - ./iptvc/:/app/ + - ./playlists/playlists.ini:${CHECKER_INIFILE:-/app/playlists.ini} + - ./playlists/channels.json:${CHECKER_TAGFILE:-/app/channels.json} depends_on: - keydb @@ -50,10 +77,10 @@ services: volumes: - ./docker/nginx/vhost.conf:/etc/nginx/conf.d/default.conf:ro - ./log/nginx:/var/log/nginx:rw - - ./svc-main:/var/www:ro + - ./web:/var/www:ro ports: - "8080:80" links: - - php + - web depends_on: - - php + - web diff --git a/docker/checker/entrypoint.sh b/docker/checker/entrypoint.sh new file mode 100755 index 0000000..b447819 --- /dev/null +++ b/docker/checker/entrypoint.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +echo "CHECKER_WAIT=$CHECKER_WAIT" +echo "CHECKER_INIFILE=$CHECKER_INIFILE" +echo "CHECKER_TAGFILE=$CHECKER_TAGFILE" + +binary="/app/bin/linux_amd64/iptvc" +args="check -i $CHECKER_INIFILE -t $CHECKER_TAGFILE" + +go get +make linux + +if [ ! -f "$binary" ]; then + echo "Not found: $binary" + exit 1 +fi + +while true; do + echo + echo "Running: $binary $args" + $binary $args + echo "Waiting $CHECKER_WAIT seconds" + sleep $CHECKER_WAIT +done diff --git a/docker/nginx/vhost.conf b/docker/nginx/vhost.conf index 2b994f4..a6e6e5b 100644 --- a/docker/nginx/vhost.conf +++ b/docker/nginx/vhost.conf @@ -20,7 +20,7 @@ server { } location ~ \.php$ { try_files $uri /index.php =404; - fastcgi_pass php:9000; + fastcgi_pass web:9000; fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; diff --git a/docker/php/dev/dockerfile b/docker/php/dev/dockerfile deleted file mode 100644 index 33fc0d8..0000000 --- a/docker/php/dev/dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM php:8.3-fpm - -RUN apt update && \ - apt upgrade -y && \ - apt install -y git unzip 7zip - -# 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 && \ - docker-php-ext-enable redis && \ - 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 - -EXPOSE 9000 -WORKDIR /var/www -CMD composer install -ENTRYPOINT php-fpm diff --git a/docker/php/dev/php.ini b/docker/php/dev/php.ini index ae2423f..c6aa22c 100644 --- a/docker/php/dev/php.ini +++ b/docker/php/dev/php.ini @@ -1,5 +1,5 @@ [PHP] -error_reporting = E_ALL +error_reporting = E_ALL & ~E_NOTICE & ~E_DEPRECATED expose_php = Off file_uploads = Off max_execution_time=-1 diff --git a/docker/php/dev/www.conf b/docker/php/dev/www.conf index 166da9a..3a6023d 100644 --- a/docker/php/dev/www.conf +++ b/docker/php/dev/www.conf @@ -19,3 +19,4 @@ 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 +php_admin_value[error_reporting] = E_ALL & ~E_NOTICE & ~E_DEPRECATED diff --git a/docker/php/prod/dockerfile b/docker/php/prod/dockerfile deleted file mode 100644 index e8e823a..0000000 --- a/docker/php/prod/dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -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 diff --git a/docker/php/prod/www.conf b/docker/php/prod/www.conf index 166da9a..3a6023d 100644 --- a/docker/php/prod/www.conf +++ b/docker/php/prod/www.conf @@ -19,3 +19,4 @@ 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 +php_admin_value[error_reporting] = E_ALL & ~E_NOTICE & ~E_DEPRECATED diff --git a/dockerfile.checker b/dockerfile.checker new file mode 100644 index 0000000..47e676c --- /dev/null +++ b/dockerfile.checker @@ -0,0 +1,14 @@ +FROM alpine:3.21 AS iptvc-compiler + +RUN apk --no-cache add \ + bash \ + tzdata \ + go \ + make + +RUN mkdir /app && \ + chmod 777 /app + +WORKDIR /app + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/dockerfile.web.dev b/dockerfile.web.dev new file mode 100644 index 0000000..9beb203 --- /dev/null +++ b/dockerfile.web.dev @@ -0,0 +1,37 @@ +FROM php:8.4-fpm AS iptv-php-dev + +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 + +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 && \ + php-fpm --nodaemonize diff --git a/dockerfile.web.prod b/dockerfile.web.prod new file mode 100644 index 0000000..84e6d7d --- /dev/null +++ b/dockerfile.web.prod @@ -0,0 +1,34 @@ +FROM php:8.4-fpm AS iptv-php-prod + +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/redis +RUN pecl channel-update pecl.php.net && \ + pecl install redis-6.1.0 + +RUN docker-php-ext-enable xdebug redis && \ + docker-php-ext-configure gd --with-jpeg && \ + docker-php-ext-install gd + +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 +RUN composer install +ENTRYPOINT php-fpm --nodaemonize diff --git a/iptv b/iptv index 46f9d46..935797d 100755 --- a/iptv +++ b/iptv @@ -1,32 +1,835 @@ -#!/bin/bash -# https://gist.github.com/anthonyaxenov/89c99e09ddb195985707e2b24a57257d +#!/usr/bin/env bash +########################################################################################## +# Скрипт управления проектом iptv.axenov.dev +# +# Copyright (c) 2025 Антон Аксенов +# MIT License, see LICENSE file for more info. +########################################################################################## -CONTAINER="iptv-php" # the name of the container in which to 'exec' something -CONFIG="$(dirname $([ -L $0 ] && readlink -f $0 || echo $0))/docker-compose.yml" # path to compose yml file -CMD="docker compose -f $CONFIG" # docker-compose command -APP_URL='http://localhost:8080/' +# shellcheck disable=SC2015,SC2103,SC2164,SC2155 +set -o pipefail -open_browser() { - if which xdg-open > /dev/null; then - xdg-open "$1" /dev/null 2>&1 & disown - elif which gnome-open > /dev/null; then - gnome-open "$1" /dev/null 2>&1 & disown +######################################################## +# Служебные исходные переменные +######################################################## + +if [[ "${BASH_SOURCE[*]}" ]]; then + [[ "${BASH_SOURCE[0]}" != "$0" ]] && echo "*** Команда source iptv запрещена ***" && exit 40 + ROOT_PATH="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +else + # script running from stdin + ROOT_PATH="$(pwd)" +fi + +IPTV_PROJECTS=("iptvc" "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" + +for sig in SIGHUP SIGINT SIGQUIT SIGABRT SIGKILL SIGTERM SIGTSTP; do + # https://faculty.cs.niu.edu/~hutchins/csci480/signals.htm + # shellcheck disable=SC2064 + trap "set +x && echo && echo && echo '*** Прервано сигналом $sig, остановка ***' && exit" $sig +done + +[[ $IPTV_DEBUG == 1 ]] && DEBUG_MODE=1 +[[ $IPTV_DEBUG -gt 1 ]] && set -x + +RAW_ARGS=("$@") +COMMAND="$1" +shift + +######################################################## +# Ввод/вывод +######################################################## + +which tput > /dev/null 2>&1 && [ "$(tput -T"$TERM" colors)" -gt 8 ] && CAN_USE_COLORS=1 || CAN_USE_COLORS=0 +IPTV_COLORS=${IPTV_COLORS:-$CAN_USE_COLORS} +[[ $IPTV_COLORS == 1 ]] && FBOLD="$(tput bold)" || FBOLD='' +[[ $IPTV_COLORS == 1 ]] && FDIM="$(tput dim)" || FDIM='' +[[ $IPTV_COLORS == 1 ]] && FRESET="$(tput sgr0)" || FRESET='' + +ask() { + IFS= read -rp "$(print "${FBOLD}$1" ): " "$2" +} + +print() { + echo -e "$*${FRESET}" +} + +debug() { + [[ "$DEBUG_MODE" != 1 ]] && return + + if [ "$2" ]; then + print "${FDIM}> ${FUNCNAME[1]:-?}():${BASH_LINENO:-?}\t$* ${FRESET}" >&2 + else + print "${FDIM}> $* ${FRESET}" >&2 fi } -case "$1" in - '' | 'help' ) echo -e "Provide one of operations: \t init, start, stop, up, down, restart, rebuild, open"; - echo "Otherwise all args will passed to 'docker exec -ti $CONTAINER ...'" ;; - 'init' ) cp src/.env.example src/.env && \ - ./iptv up && \ - ./iptv composer i && \ - echo "Project started successfully! $APP_URL" ;; - 'up' ) $CMD up -d --build && ./iptv open ;; # build and start containers - 'down' ) $CMD down --remove-orphans ;; # stop and remove containers - 'start' ) $CMD start ;; # start containers - 'stop' ) $CMD stop ;; # stop containers - 'restart' ) $CMD stop && $CMD start ;; # restart containers - 'rebuild' ) $CMD down --remove-orphans && $CMD up -d --build ;; # rebuild containers - 'open' ) open_browser $APP_URL && echo -e "\nYou're welcome!\n\t$APP_URL" ;; - * ) docker exec -ti $CONTAINER $* ;; # exec anything else in container +var_dump() { + debug "$1 = ${!1}" +} + +inform() { + print "$*${FRESET}" +} + +subtitle() { + echo + inform "${FBOLD}$*${FRESET}" +} + +title() { + subtitle "$@" + echo +} + +success() { + print "${FBOLD}$*${FRESET}" +} + +warn() { + print "${FBOLD}Внимание!${FRESET} $*${FRESET}" +} + +error() { + print "${FBOLD}Ошибка:${FRESET} $*${FRESET}" >&2 + print_stacktrace +} + +die() { + error "${1:-halted}" + exit "${2:-255}" +} + +print_stacktrace() { + [[ "$DEBUG_MODE" != 1 ]] && return + + local i + local stack_size=${#FUNCNAME[@]} + debug "Callstack:" + # for (( i=$stack_size-1; i>=1; i-- )); do + for (( i=1; i /dev/null +} + +# Проверяет соответствие строки $1 регулярному выражению $2 +regex_match() { + [[ "$1" =~ ^$2$ ]] +} + +# Проверяет соответствие строки $1 регулярному выражению $2 с помощью grep +grep_match() { + printf "%s" "$1" | grep -qE "$2" >/dev/null 2>&1 +} + +# Возвращает название текущей ОС +get_os() { + case "$(uname -s)" in + Linux*) echo Linux ;; + Darwin*) echo Macos ;; + CYGWIN*) echo Cygwin ;; + MINGW*) echo MinGw ;; + MSYS_NT*) echo Git ;; + *) return 1 ;; + esac +} + +# Обрезает пробельные символы с начала и с конца строки +trim() { + echo "$1" | xargs +} + +######################################################## +# Функции парсинга аргументов +# https://gist.axenov.dev/anthony/sh-args +######################################################## + +# Парсит короткий аргумент +arg() { + [ "$1" ] || { echo "Argument name is not specified!" >&2 && exit 1; } + local arg_name="${1:0:1}" # first character of argument name to find + local is_flag="$2" || 0 # 1 if we need just find a flag, 0 to get a value + local var_name="$3" || 0 # variable name to return value into or 0 to echo it in stdout + local value= # initialize empty value to check if we found one later + local arg_found=0 # marker of found argument + + for idx in "${!RAW_ARGS[@]}"; do # going through all args + local arg_search=${RAW_ARGS[idx]} # get current argument + + # skip $arg_search if it starts with '--' or letter + grep_match "$arg_search" "^(\w|--)" && continue + + # clear $arg_search from special and duplicate characters, e.g. 'fas-)dfs' will become 'fasd' + local arg_chars="$(printf "%s" "$arg_search" \ + | tr -s "[$arg_search]" 2>/dev/null \ + | tr -d "[:punct:][:blank:]" 2>/dev/null)" + + # if $arg_name is not one of $arg_chars the skip it + grep_match "-$arg_name" "^-[$arg_chars]$" || continue + arg_found=1 + + # then return '1'|'0' back into $value if we need flag or next arg value otherwise + [[ "$is_flag" == 1 ]] && value=1 || value="${RAW_ARGS[idx+1]}" + break + done + + [[ "$is_flag" == 1 ]] && [[ -z "$value" ]] && value=0; + + # if value we found is empty or looks like another argument then exit with error message + if [ "$arg_found" = 1 ] && ! grep_match "$value" "^[[:graph:]]+$" || grep_match "$value" "^--?\w+$"; then + echo "ERROR: Argument '-$arg_name' must have correct value!" >&2 && exit 1 + fi + + # return '$value' back into $var_name (if exists) or echo in stdout + [ "$var_name" ] && eval "$var_name='$value'" || echo "$value" +} + +# Парсит длинный аргумент +argl() { + [ "$1" ] || { echo "Argument name is not specified!" >&2 && exit 1; } + local arg_name="$1" # argument name to find + local is_flag="$2" || 0 # 1 if we need just find a flag, 0 to get a value + local var_name="$3" || 0 # variable name to return value into or 0 to echo it in stdout + local value= # initialize empty value to check if we found one later + local arg_found=0 # marker of found argument + + for idx in "${!RAW_ARGS[@]}"; do # going through all args + local arg_search="${RAW_ARGS[idx]}" # get current argument + + if [ "$arg_search" = "--$arg_name" ]; then # if current arg begins with two dashes + # then return '1' back into $value if we need flag or next arg value otherwise + [[ "$is_flag" == 1 ]] && value=1 || value="${RAW_ARGS[idx+1]}" + break # stop the loop + elif grep_match "$arg_search" "^--$arg_name=.+$"; then # check if $arg like '--foo=bar' + # then return '1' back into $value if we need flag or part from '=' to arg's end as value otherwise + [[ "$is_flag" == 1 ]] && value=1 || value="${arg_search#*=}" + break # stop the loop + fi + done + + [[ "$is_flag" == 1 ]] && [[ -z "$value" ]] && value=0; + + # if value we found is empty or looks like another argument then exit with error message + if [ "$arg_found" = 1 ] && ! grep_match "$value" "^[[:graph:]]+$" || grep_match "$value" "^--?\w+$"; then + echo "ERROR: Argument '--$arg_name' must have correct value!" >&2 && exit 1; + fi + + # return '$value' back into $var_name (if exists) or echo in stdout + [ "$var_name" ] && eval "$var_name='$value'" || echo "$value" +} + +######################################################## +# Функции контроля системных пакетов +######################################################## + +# Проверяет наличие команды в системе +installed() { + command -v "$1" >/dev/null 2>&1 +} + +# Проверяет наличие пакета в системе +installed_pkg() { + dpkg --list | grep -qw "ii $1" +} + +# Требует наличие пакета в системе +require() { + sw=() + for package in "$@"; do + # if ! installed "$package" && ! installed_pkg "$package"; then + if ! installed "$package"; then + sw+=("$package") + fi + done + if [ ${#sw[@]} -gt 0 ]; then + die "Это ПО должно быть установлено в системе:\n${sw[*]}" 200 + fi +} + +######################################################## +# Функции для работы с git +######################################################## + +# Проверяет, является ли директория git-репозиторием +git.is_repo() { + require git + is_dir "$1/" && is_dir "$1/.git/" +} + +# Клонирует репозиторий +git.clone() { + require git + cmd="git clone $*" + debug "Команда: $cmd" + $cmd +} + +######################################################## +# Функции для работы с docker +######################################################## + +# Вызывает корректную команду docker compose +docker.compose() { + require docker + + args=${*/--profiles=[a-zA-Z_,0-9]*/} + + if docker compose &>/dev/null; then + local cmd="docker compose $args" + elif installed_pkg "docker-compose"; then + local cmd="docker-compose $args" + warn + warn "docker-compose v1 устарел и не поддерживается, его поведение непредсказуемо." + warn "Обнови docker согласно документации: https://docs.docker.com/engine/install/" + warn + else + error "Должен быть установлен docker-compose-plugin!" + die "Установи docker согласно документации: https://docs.docker.com/engine/install/" 2 + fi + + debug "Команда: $cmd" + $cmd +} + +# Выводит информацию о контейнере +docker.inspect() { + cmd="docker inspect $*" + debug "Команда: $cmd" + $cmd 2>/dev/null +} + +# Строит образы согласно файлов iptv-docker/docker-compose*.yml +docker.build_base_images() { + # for file in compose*.yml; do + # subtitle "Построение базовых образов: $file" + # docker.compose -f "$file" build + # done + subtitle "Построение образов" + docker.compose build + + success "Базовые образы построены" +} + +# Выполняет команду в контейнере от имени root +docker.exec() { + cmd="docker exec -u root -it $*" + debug "Команда: $cmd" + $cmd +} + +# Выполняет команду в контейнере от имени www-data +docker.exec_www() { + cmd="docker exec -u www-data -it $*" + debug "Команда: $cmd" + $cmd +} + +######################################################## +# Хелперы для обработки команд +######################################################## + +# Возвращает ssh-адрес к репозиторию проекта +project_url_ssh() { + echo "$IPTV_GITEA_URL_SSH/${IPTV_PROJECTS[$1]}.git" +} + +# Возвращает https-адрес к репозиторию проекта +project_url_https() { + echo "$IPTV_GITEA_URL_HTTPS/${IPTV_PROJECTS[$1]}" +} + +# Копирует .env.example в .env, если возможно +prepare_dot_env() { + if is_file .env.example && ! is_file .env; then + debug "Копирование .env.example => .env" + cp .env.example .env + fi +} + +# Клонирует проект +project_clone() { + local repo_url="$1"; shift + local repo_path="$1"; shift + + if git.is_repo "$repo_path"; then + debug "Репозиторий уже существует: $repo_path" + else + debug "Репозиторий будет склонирован: $repo_path" + git.clone "$repo_url" "$repo_path" "$@" + fi +} + +# Проверяет передачу флагов -h|--help в команду и выводит справку +process_help_arg() { + command="${FUNCNAME[1]}" + need_help=$(arg help 1) + [[ "$need_help" -eq 0 ]] && need_help=$(argl help 1) + [[ "$need_help" -eq 1 ]] && help "$command" +} + +# Выводит список сервисов +list_services_compose() { + services=$(docker.compose --profile full config --services | sort) + IFS=$'\n' sorted=("${services[@]}") + unset IFS + for name in "${sorted[@]}"; do + print " $name" + done +} + +# Возвращает корректное название сервиса из композа +find_service_compose() { + svc="$1" + [ -z "$svc" ] && die "неизвестный сервис" 2 + + for known in $(docker.compose config --services); do + if [ "$known" = "$svc" ]; then + debug "Сервис '$svc' найден в композе" + echo "iptv-$known" + exit + fi + done + + debug "Сервис '$svc' не найден в композе" + return 1 +} + +# Возвращает корректные названия сервисов из композа +find_services_compose() { + local services='' + + [ "$*" ] && for svc in "$@"; do + grep_match "$svc" "^--?.*" && continue + var_dump svc + svc="$(find_service_compose "$svc")" + services="$services iptv-$svc" + done + + trim "$services" +} + +######################################################## +# Главные функции обработки команд +######################################################## + +# Инициализирует репозиторий iptv-docker и все вложенные в него +init() { + 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 + + #TODO установка ПО + + subtitle "Подготовка локальной среды..." + + project_clone "$IPTV_DOCKER_URL_SSH" "$docker_repo_path" + + cd "$docker_repo_path" + 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 project_repo_url=$(project_url_ssh "$repo_name") + debug "Известная ссылка на репозиторий: $project_repo_url" + + project_clone "$project_repo_url" "$project_repo_path" + cd "$project_repo_path" + + prepare_dot_env + + cd - >/dev/null + success "Репозиторий $repo_name готов" + counter=$((counter+1)) + done + + success "Локальная среда развёрнута: $docker_repo_path" + + cd "$docker_repo_path" + docker.build_base_images && up + + success "Проверь корректность значений, указанных в файлах .env, и выполни ${FBOLD}./iptv rebuild${FRESET} при необходимости." +} + +# Создаёт и запускает контейнеры +up() { + process_help_arg + subtitle "Создание и запуск контейнеров" + + argl profiles 0 profiles + + local services='' + [ "$*" ] && services="$(find_services_compose "$@")" + + COMPOSE_PROFILES="$profiles" docker.compose up "$services" --build --detach --remove-orphans && \ + success 'Среда запущена успешно' +} + +# Запускает созданные контейнеры +start() { + process_help_arg + subtitle "Запуск созданных ранее контейнеров" + + profiles="$(argl profiles 0)" + + local services='' + [ "$*" ] && services="$(find_services_compose "$@")" + + COMPOSE_PROFILES="$profiles" docker.compose start "$services" && \ + success 'Среда запущена успешно' +} + +# Останавливает и удаляет контейнеры +down() { + process_help_arg + subtitle "Остановка и удаление контейнеров" + + argl profiles 0 profiles + [[ -z "$profiles" ]] && profiles="full" + + local services='' + [ "$*" ] && services="$(find_services_compose "$@")" + + COMPOSE_PROFILES="$profiles" docker.compose down "$services" --remove-orphans && \ + success 'Среда остановлена успешно' +} + +# Останавливает контейнеры +stop() { + process_help_arg + subtitle "Остановка контейнеров" + + profiles=$(argl profiles 0) + [[ -z "$profiles" ]] && profiles="full" + + local services='' + [ "$*" ] && services="$(find_services_compose "$@")" + + COMPOSE_PROFILES="$profiles" docker.compose stop "$services" && \ + success 'Среда остановлена успешно' +} + +# Останавливает, удаляет, создаёт и запускает контейнеры +rebuild() { + process_help_arg + + is_full=$(arg full 1) + [ "$is_full" = 0 ] && is_full=$(argl full 1) + + [ -n "$*" ] && down "$@" + [ "$is_full" = 1 ] && docker.build_base_images + + up "$@" +} + +# Останавливает и запускает созданные контейнеры +restart() { + process_help_arg + stop "$@" && start "$@" +} + +# Останавливает среду, удаляет все контейнеры и образы +purge() { + process_help_arg + + subtitle "Удаление docker-образов и контейнеров" + + down + docker rmi "$(docker image list | grep iptv- | awk '{print $3}')" + docker system prune --all --force + + success 'Образы удалены успешно' +} + +# Выводит логи сервиса +logs() { + process_help_arg + [ -z "$1" ] && die "не указан сервис" 8 + + svc=$(find_service_compose "$1") + [ -z "$svc" ] && die "неизвестный сервис" 3 + + subtitle "Логи сервиса $svc" + + shift + cmd="docker logs $svc $*" + debug "Команда: $cmd" + $cmd +} + +# Выводит статистику потребления ресурсов контейнерами +stats() { + subtitle "Состояние контейнеров" + + docker.compose stats "$@" +} + + +######################################################## +# Команды справки +######################################################## + +help() { + print "${FBOLD}Среда разработки iptv.axenov.dev" + is_function "help.$1" && help."$1" && exit + print "Использование:" + print " ./iptv КОМАНДА [АРГУМЕНТЫ]" + print + print "Доступные КОМАНДЫ:" + print " h|help - вывести это сообщение и выйти" + print " init - инициализировать окружение" + print " u|up - построить образы, контейнеры и запустить их" + print " start - запустить построенные ранее контейнеры" + print " d|down - остановить и удалить контейнеры" + print " stop - остановить контейнеры" + print " r|rebuild - выполнить 'down' и 'up'" + print " restart - выполнить 'stop' и 'start'" + print " purge - удалить контейнеры и образы" + print " update - обновить репозитории" + print " info - вывести версии ПО контейнера сервиса" + print " f|flame - сгенерировать флеймграф из трейса xdebug" + print " fix - исправить некоторые проблемы окружения" + print " exec - выполнить произвольную команду в контейнере" + print " logs - вывести логи контейнера" + print " profiles - вывести список профилей" + print " services - вывести список сервисов профиля" + print " info - вывести информацию о ПО в контейнере" + print " status - вывести статус сервиса и его репозитория" + print " stats - вывести информацию о потреблении ресурсов окружением" + print + print "Доступные АРГУМЕНТЫ:" + print " -h|--help - вывести справку по любой КОМАНДЕ" + print + print "Любая КОМАНДА может также иметь свои АРГУМЕНТЫ." + print "Некоторые КОМАНДЫ могут также поддерживать АРГУМЕНТЫ 'docker' и его субсомманд." + print + print "См. './iptv help КОМАНДА' или './iptv КОМАНДА -h|--help' для справки." +} + +help.help() { + print "КОМАНДА: help" + print "Отображает справку по скрипту и его командам." + print + print "Использование:" + print " ./iptv help [КОМАНДА]" + print " ./iptv КОМАНДА [-h|--help]" + print + print "Если КОМАНДА не указана, будет отображена справка по скрипту со списком допустимых КОМАНД." +} + +help.init() { + print "КОМАНДА: init" + print "Инициализирует окружение разработки. Клонирует репозиторий iptv-docker, клонирует" + print "внутрь исходники всех сервисов и создаёт заготовки файлов .env для них. Можно" + print "использовать для подгрузки свежих сервисов, ещё не развёрнутых локально." + print + print "Использование:" + print " ./iptv init [АРГУМЕНТЫ]" + print + print "Доступные АРГУМЕНТЫ:" + print " -n|--name ИМЯ - установить ИМЯ в качестве имени пользователя git" + print " -e|--email ПОЧТА - установить ПОЧТУ в качестве email пользователя git" + print " -h|--help - вывести это сообщение и выйти" + print + print "Если --name и/или --email не переданы, то данные будут взяты из глобального конфига git." + print + print "ИМЯ и ПОЧТА проверяются на валидность:" + print " - ИМЯ должно содержать имя и фамилию на кириллице, разделённые пробелом;" + print " - ПОЧТА должна быть корпоративной (@bars.group или @bars-open.ru)." + print + print "Если ИМЯ или ПОЧТА некорректны, КОМАНДА завершится с ошибкой и предложением передать" + print "явно ИМЯ и ПОЧТУ с помощью аргументов." + print + print "После выполнения КОМАНДЫ необходимо указать корректные данные в файлах .env сервисов." +} + +help.up() { + print "КОМАНДА: up" + print "Выполняет перестроение образов (включая базовые, при необходимости)" + print "создание и запуск контейнеров." + print + print "Использование:" + print " ./iptv up [АРГУМЕНТЫ] [СЕРВИСЫ...]" + print + print "Доступные АРГУМЕНТЫ:" + print " -h|--help - вывести это сообщение и выйти" + print + print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним." + print + print "СЕРВИСЫ можно передавать без префикса 'iptv-'" +} + +help.start() { + print "КОМАНДА: start" + print "Выполняет запуск построенных ранее контейнеров." + print + print "Использование:" + print " ./iptv start [АРГУМЕНТЫ] [СЕРВИСЫ...]" + print + print "Доступные АРГУМЕНТЫ:" + print " -h|--help - вывести это сообщение и выйти" + print + print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним." + print + print "СЕРВИСЫ можно передавать без префикса 'iptv-'" +} + +help.down() { + print "КОМАНДА: down" + print "Выполняет остановку и удаление запущенных контейнеров." + print + print "Использование:" + print " ./iptv down [АРГУМЕНТЫ] [СЕРВИСЫ...]" + print + print "Доступные АРГУМЕНТЫ:" + print " -h|--help - вывести это сообщение и выйти" + print + print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним." + print + print "СЕРВИСЫ можно передавать без префикса 'iptv-'" +} + +help.stop() { + print "КОМАНДА: stop" + print "Выполняет остановку запущенных контейнеров." + print + print "Использование:" + print " ./iptv stop [АРГУМЕНТЫ] [СЕРВИСЫ...]" + print + print "Доступные АРГУМЕНТЫ:" + print " -h|--help - вывести это сообщение и выйти" + print + print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним." + print + print "СЕРВИСЫ можно передавать без префикса 'iptv-'" +} + +help.rebuild() { + print "КОМАНДА: rebuild" + print "Выполняет 'down' и 'up'." + print + print "Использование:" + print " ./iptv rebuild [АРГУМЕНТЫ] [СЕРВИСЫ...]" + print + print "Доступные АРГУМЕНТЫ:" + print " -h|--help - вывести это сообщение и выйти" + print + print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним." + print + print "СЕРВИСЫ можно передавать без префикса 'iptv-'" +} + +help.restart() { + print "КОМАНДА: restart" + print "Выполняет 'stop' и 'start'." + print + print "Использование:" + print " ./iptv restart [АРГУМЕНТЫ] [СЕРВИСЫ...]" + print + print "Доступные АРГУМЕНТЫ:" + print " -h|--help - вывести это сообщение и выйти" + print + print "Если указан один и более СЕРВИСОВ, то КОМАНДА будет применена только к ним." + print + print "СЕРВИСЫ можно передавать без префикса 'iptv-'" +} + +help.purge() { + print "КОМАНДА: purge" + print "Удаляет все образы и контейнеры 'iptv-*'." + print + print "Использование:" + print " ./iptv purge [АРГУМЕНТЫ]" + print + print "Доступные АРГУМЕНТЫ:" + print " -h|--help - вывести это сообщение и выйти" +} + +help.logs() { + print "КОМАНДА: logs" + print "Выводит логи указанного СЕРВИСА." + print + print "Использование:" + print " ./iptv logs СЕРВИС [АРГУМЕНТЫ]" + print + print "Доступные АРГУМЕНТЫ:" + print " -h|--help - вывести это сообщение и выйти" + print + print "Поддерживаются АРГУМЕНТЫ 'docker logs'." + print + print "СЕРВИС можно передавать без префикса 'iptv-'" +} + +help.stats() { + print "КОМАНДА: stats" + print "Выводит потребление ресурсов окружением разработки." + print + print "Использование:" + print " ./iptv stats [АРГУМЕНТЫ]" + print + print "Доступные АРГУМЕНТЫ:" + print " -h|--help - вывести это сообщение и выйти" + print + print "Поддерживаются АРГУМЕНТЫ 'docker compose stats'." + print "Рекомендуется использовать вместе с '--no-stream'." +} + +######################################################## +# Точка входа +######################################################## + +debug "Директория выполнения: $ROOT_PATH/$IPTV_DIR" + +case "$COMMAND" in + h|help ) help "$1" ;; + init ) init "$@" ;; + u|up ) up "$@" ;; + start ) start "$@" ;; + d|down ) down "$@" ;; + stop ) stop "$@" ;; + r|rebuild ) rebuild "$@" ;; + restart ) restart "$@" ;; + purge ) purge ;; + logs ) logs "$@" ;; + stats ) stats "$@" ;; + * ) die "неизвестная команда. Смотри './iptv help' для справки." ;; esac