diff --git a/.docker/openresty/Dockerfile b/.docker/openresty/Dockerfile new file mode 100644 index 00000000..477be2d8 --- /dev/null +++ b/.docker/openresty/Dockerfile @@ -0,0 +1,10 @@ +FROM openresty/openresty:alpine + +# 安装基础依赖 +RUN apk add --no-cache gettext bash + +# 拷贝 entrypoint +COPY ./entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +CMD ["/bin/sh", "/usr/local/bin/entrypoint.sh"] diff --git a/.docker/openresty/certs/cloudflare/.gitkeep b/.docker/openresty/certs/cloudflare/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.docker/openresty/entrypoint.sh b/.docker/openresty/entrypoint.sh new file mode 100644 index 00000000..6989f7fd --- /dev/null +++ b/.docker/openresty/entrypoint.sh @@ -0,0 +1,57 @@ +#!/bin/sh +set -e + +if [ -z "$DOMAIN" ]; then + echo "❌ 错误:必须设置 DOMAIN 环境变量!" + exit 1 +fi + +echo "当前域名是: $DOMAIN" + +# 设定证书目录 +CLOUDFLARE_CERT_DIR="/certs/cloudflare" +FINAL_CERT_DIR="/certs/live" +FULLCHAIN="fullchain.pem" +PRIVATE_KEY="private.key" + +# 检查 Cloudflare 证书是否存在 +if [ -f "$FINAL_CERT_DIR/$FULLCHAIN" ] && [ -f "$FINAL_CERT_DIR/$PRIVATE_KEY" ]; then + echo "ssl certs already exists at: ${FINAL_CERT_DIR}" +else + if [ -f "$CLOUDFLARE_CERT_DIR/$FULLCHAIN" ] && [ -f "$CLOUDFLARE_CERT_DIR/$PRIVATE_KEY" ]; then + echo "⚡️ Cloudflare certs exists at: $CLOUDFLARE_CERT_DIR, copy to: $FINAL_CERT_DIR ..." + mkdir -p "$FINAL_CERT_DIR" + cp "$CLOUDFLARE_CERT_DIR/$FULLCHAIN" "$FINAL_CERT_DIR/$FULLCHAIN" + cp "$CLOUDFLARE_CERT_DIR/$PRIVATE_KEY" "$FINAL_CERT_DIR/$PRIVATE_KEY" + else + echo "🔍 Cloudflare certs not exists at: $CLOUDFLARE_CERT_DIR,use acme.sh to apply ..." + + # 安装 acme.sh(如果还没装) + if [ ! -d "/root/.acme.sh" ]; then + curl https://get.acme.sh | sh + source ~/.bashrc + fi + + # 申请证书 + ~/.acme.sh/acme.sh --issue --standalone -d "$DOMAIN" --keylength ec-256 + + # 安装证书到目标目录 + ~/.acme.sh/acme.sh --install-cert -d "$DOMAIN" --ecc \ + --key-file "$FINAL_CERT_DIR/$PRIVATE_KEY" \ + --fullchain-file "$FINAL_CERT_DIR/$FULLCHAIN" + fi +fi + +echo "✅ ssl certs done." + +# 组合子域名变量 +export PHPMYADMIN_SERVER_NAME="phpmyadmin.${DOMAIN}" + +# 清空旧配置 +rm -rf /etc/nginx/conf.d/*.conf + +# 生成配置 +envsubst '$DOMAIN' < /etc/nginx/conf.d/sites/app.conf.template > /etc/nginx/conf.d/app.conf +envsubst '$PHPMYADMIN_SERVER_NAME' < /etc/nginx/conf.d/sites/phpmyadmin.conf.template > /etc/nginx/conf.d/phpmyadmin.conf + +exec openresty -g 'daemon off;' diff --git a/.docker/openresty/sites/app.conf.template b/.docker/openresty/sites/app.conf.template new file mode 100644 index 00000000..9654b35e --- /dev/null +++ b/.docker/openresty/sites/app.conf.template @@ -0,0 +1,41 @@ +server { + listen 443 ssl http2; + server_name ${DOMAIN}; + + root /var/www/html/public; + index index.php index.html; + + ssl_certificate /certs/live/fullchain.pem; + ssl_certificate_key /certs/live/privkey.pem; + + location = /favicon.ico { + log_not_found off; + access_log off; + } + + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + + location / { + try_files $uri $uri/ /nexus.php?$query_string; + } + + # Filament + location ^~ /filament { + try_files $uri $uri/ /nexus.php$is_args$args; + } + + location ~ \.php$ { + fastcgi_pass php:9000; + fastcgi_index index.php; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param REQUEST_ID $request_id; + } + + error_log /dev/stderr; + access_log /dev/stdout; +} diff --git a/.docker/openresty/sites/phpmyadmin.conf.template b/.docker/openresty/sites/phpmyadmin.conf.template new file mode 100644 index 00000000..8380d06f --- /dev/null +++ b/.docker/openresty/sites/phpmyadmin.conf.template @@ -0,0 +1,16 @@ +server { + listen 443 ssl http2; + server_name ${PHPMYADMIN_SERVER_NAME}; + + ssl_certificate /certs/live/fullchain.pem; + ssl_certificate_key /certs/live/privkey.pem; + + location / { + proxy_pass http://phpmyadmin:80; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } + + error_log /dev/stderr; + access_log /dev/stdout; +} diff --git a/.docker/php/Dockerfile b/.docker/php/Dockerfile new file mode 100644 index 00000000..83e1458b --- /dev/null +++ b/.docker/php/Dockerfile @@ -0,0 +1,71 @@ +# 👷‍♀️ 第一阶段:构建阶段,包含所有开发依赖 +FROM php:8.2-fpm-alpine AS builder + +RUN apk add --no-cache \ + $PHPIZE_DEPS \ + libzip-dev \ + libpng-dev \ + libjpeg-turbo-dev \ + freetype-dev \ + icu-dev \ + libxml2-dev \ + libwebp-dev \ + gmp-dev \ + oniguruma-dev \ + linux-headers + +RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp + +RUN docker-php-ext-install -j$(nproc) \ + bcmath \ + pdo_mysql \ + mysqli \ + gd \ + pcntl \ + sockets \ + gmp \ + zip \ + intl \ + opcache + +# 安装 redis 扩展 +RUN pecl install redis && docker-php-ext-enable redis + +# 👨‍🍳 第二阶段:运行阶段,仅包含必要运行环境 +FROM php:8.2-fpm-alpine + +# 复制已构建的扩展 +COPY --from=builder /usr/local/lib/php/extensions /usr/local/lib/php/extensions +COPY --from=builder /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/ + +# 安装运行时所需依赖 +RUN apk add --no-cache \ + wget \ + rsync \ + libzip \ + libpng \ + libjpeg-turbo \ + libwebp \ + freetype \ + icu \ + libxml2 \ + gmp \ + oniguruma + +# 配置 www.conf +RUN sed -i \ + -e 's/;catch_workers_output = .*/catch_workers_output = yes/g' \ + -e 's/;php_admin_flag\[log_errors\] = .*/php_admin_flag\[log_errors\] = on/g' \ + -e 's/;php_admin_value\[error_log\] = .*/php_admin_value\[error_log\] = \/dev\/stderr/g' \ + /usr/local/etc/php-fpm.d/www.conf + +# 配置 PHP 错误日志输出到 stderr +RUN echo "error_log = /dev/stderr" >> /usr/local/etc/php/conf.d/error-logging.ini + +# 安装 Composer +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer + +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +ENTRYPOINT ["entrypoint.sh"] diff --git a/.docker/php/entrypoint.sh b/.docker/php/entrypoint.sh new file mode 100644 index 00000000..10d8038f --- /dev/null +++ b/.docker/php/entrypoint.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +ROOT_PATH="/var/www/html" + +SOURCE_DIR="${ROOT_PATH}/nexus/Install/install" +TARGET_DIR="${ROOT_PATH}/public" +ENV_FILE="${ROOT_PATH}/.env" +VENDOR_DIR="${ROOT_PATH}/vendor" +#COMPOSER_FILE="${ROOT_PATH}/composer.json" + +# 检查目标文件是否存在 +if [ ! -f "$ENV_FILE" ]; then + echo "🔧 .env file: $ENV_FILE not exists, copy $SOURCE_DIR to $TARGET_DIR ..." + cp -r "$SOURCE_DIR" "$TARGET_DIR" +else + echo "✅ .env file: $ENV_FILE already exists, skip copy install file ..." +fi + +# composer install +if [ ! -d "$VENDOR_DIR" ]; then + echo "🔧 vendor dir: $VENDOR_DIR not exists, run composer install ..." + composer install --working-dir=${ROOT_PATH} +else + echo "✅ vendor dir: $VENDOR_DIR already exists, skip run composer install ..." +fi + +# 最后启动 PHP-FPM +exec php-fpm diff --git a/.gitignore b/.gitignore index 8a47346e..5987a483 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,10 @@ auth.json /.history /public/dev.php crowdin.yml +install.lock +update.lock +.env.bak +attachments +bitbucket +/public/attachments +/public/bitbucket diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..674f0789 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,157 @@ +services: + php: + build: + context: ./.docker/php + container_name: nexusphp-php + volumes: + - .:/var/www/html + depends_on: + - mysql + - redis + logging: + driver: "json-file" + options: + max-size: "1024m" + max-file: "3" + networks: + - appnet + + queue: + image: nexusphp-php + container_name: nexusphp-queue + command: > + sh -c ' + echo "Start Queue Worker..."; + while true; do + if [ -f /var/www/html/.env ] && [ -d /var/www/html/vendor ]; then + echo "[Queue] Run queue:work at $(date '+%Y-%m-%d %H:%M:%S')"; + php artisan queue:work --verbose --tries=3; + else + echo "[Queue] .env or vendor not exists,wait 5 seconds ..."; + sleep 5; + fi + done + ' + volumes: + - .:/var/www/html + depends_on: + - php + - redis + - mysql + logging: + driver: "json-file" + options: + max-size: "1024m" + max-file: "3" + networks: + - appnet + + scheduler: + image: nexusphp-php + container_name: nexusphp-scheduler + command: > + sh -c ' + echo "Start Scheduler ..."; + while true; do + if [ -f /var/www/html/.env ] && [ -d /var/www/html/vendor ]; then + echo "[Scheduler] Run schedule:run at $(date '+%Y-%m-%d %H:%M:%S')"; + php artisan schedule:run --verbose --no-interaction; + sleep 60; + else + echo "[Scheduler] .env or vendor not exists,wait 5 seconds..."; + sleep 5; + fi + done + ' + volumes: + - .:/var/www/html + depends_on: + - php + - redis + - mysql + logging: + driver: "json-file" + options: + max-size: "1024m" + max-file: "3" + networks: + - appnet + + openresty: + build: + context: ./.docker/openresty + container_name: nexusphp-openresty + ports: + - "8080:80" + environment: + DOMAIN: ${DOMAIN} + volumes: + - ./.docker/openresty/sites:/etc/nginx/conf.d/sites + - ./.docker/openresty/certs:/certs + - ./.docker/openresty/entrypoint.sh:/usr/local/bin/entrypoint.sh + - .:/var/www/html + depends_on: + - php + - phpmyadmin + command: ["/bin/sh", "/usr/local/bin/entrypoint.sh"] + logging: + driver: "json-file" + options: + max-size: "1024m" + max-file: "3" + networks: + - appnet + + redis: + image: redis:alpine + container_name: nexusphp-redis + command: redis-server + volumes: + - redis-data:/data + logging: + driver: "json-file" + options: + max-size: "1024m" + max-file: "3" + networks: + - appnet + + mysql: + image: mysql:9 + container_name: nexusphp-mysql + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: nexusphp + MYSQL_USER: nexusphp + MYSQL_PASSWORD: secret + volumes: + - mysql-data:/var/lib/mysql + logging: + driver: "json-file" + options: + max-size: "1024m" + max-file: "3" + networks: + - appnet + + phpmyadmin: + image: phpmyadmin/phpmyadmin + container_name: nexusphp-phpmyadmin + environment: + PMA_HOST: mysql + depends_on: + - mysql + logging: + driver: "json-file" + options: + max-size: "1024m" + max-file: "3" + networks: + - appnet + +volumes: + mysql-data: + redis-data: + +networks: + appnet: diff --git a/include/constants.php b/include/constants.php index a2553fe0..a64ded92 100644 --- a/include/constants.php +++ b/include/constants.php @@ -1,6 +1,6 @@ now(), ]; diff --git a/public/announce.php b/public/announce.php index c3afac94..1c1a75a8 100644 --- a/public/announce.php +++ b/public/announce.php @@ -118,14 +118,14 @@ if (!$az = $Cache->get_value('user_passkey_'.$passkey.'_content')){ } if (!$az) { $redis->set("$passkeyInvalidKey:$passkey", TIMENOW, ['ex' => 24*3600]); - warn("Invalid passkey! Re-download the .torrent from $BASEURL"); + err("Invalid passkey! Re-download the .torrent from $BASEURL"); } if ($az["enabled"] == "no") - warn("Your account is disabled!", 300); + err("Your account is disabled!", 300); elseif ($az["parked"] == "yes") - warn("Your account is parked! (Read the FAQ)", 300); + err("Your account is parked! (Read the FAQ)", 300); elseif ($az["downloadpos"] == "no") - warn("Your downloading privileges have been disabled! (Read the rules)", 300); + err("Your downloading privileges have been disabled! (Read the rules)", 300); $userid = intval($az['id'] ?? 0); unset($GLOBALS['CURUSER']);