|
#!/bin/bash |
|
|
|
#=============================================================================== |
|
# lara - Công cụ quản lý website PHP/Laravel trong một file duy nhất |
|
# |
|
# Phiên bản : 1.13.0 (xem 'lara changelog' để biết lịch sử thay đổi) |
|
# Hệ điều hành : Ubuntu 18.04 - 26.04 LTS (tương thích Debian) |
|
# Cấu trúc : mã nguồn /var/www/[domain] |
|
# socket /var/run/php/[php-version]_[domain].sock |
|
# nginx /etc/nginx/sites-available/[domain].conf (chỉ HTTP) |
|
# fpm pool /etc/php/[php-version]/fpm/pool.d/[domain].conf |
|
# |
|
# Mỗi website chạy dưới một user hệ thống riêng (lv_<domain>) để các website |
|
# trên cùng máy chủ không thể đọc dữ liệu của nhau. User nginx (www-data) |
|
# chỉ kết nối tới socket php-fpm của website và đọc tài nguyên tĩnh trong |
|
# public/ thông qua group www-data trên thư mục mã nguồn. |
|
# |
|
# Cách dùng: |
|
# sudo lara # mở menu tương tác (phím ↑/↓ hoặc gõ số) |
|
# |
|
# sudo ./lara.sh install # cài vào /usr/local/bin/lara + banner chào mừng SSH |
|
# sudo lara create <domain> --php <7.4|8.4> --laravel <8-13> [--repo <git-url>] [--db] |
|
# # có --repo: clone mã nguồn từ git |
|
# # không có --repo: chỉ tạo thư mục trống |
|
# # có --db: tạo database + user MySQL/MariaDB riêng |
|
# sudo lara deploy <domain> |
|
# sudo lara remove <domain> |
|
# sudo lara fixperms <domain> # sửa lại quyền (sau khi upload code/SFTP) |
|
# sudo lara cert <list|status|issue|renew|revoke> [domain] [--no-www] [--force] |
|
# lara list |
|
# lara check [--php <7.4|8.4>] |
|
# lara version # in phiên bản hiện tại |
|
# lara changelog # xem lịch sử thay đổi giữa các bản |
|
# sudo lara update # tự tải bản mới nhất từ gist công khai |
|
# sudo lara uninstall # gỡ /usr/local/bin/lara (website giữ nguyên) |
|
# |
|
# Thêm --dry-run vào bất kỳ lệnh nào để CHẠY THỬ: các bước kiểm tra vẫn chạy |
|
# thật, còn mọi thay đổi hệ thống chỉ được in ra chứ không thực thi. |
|
# Ví dụ: lara create example.com --php 8.4 --laravel 12 --db --dry-run |
|
#=============================================================================== |
|
|
|
set -euo pipefail |
|
|
|
LARA_VERSION="1.13.0" |
|
INSTALL_PATH="/usr/local/bin/lara" |
|
MOTD_PATH="/etc/update-motd.d/99-lara" # banner chào mừng khi đăng nhập SSH |
|
DRY_RUN="" # đặt bằng cờ --dry-run: chỉ hiển thị thay đổi, không thực thi |
|
|
|
# Nguồn tự cập nhật ('lara update'): URL raw "MỚI NHẤT" của gist CÔNG KHAI chỉ |
|
# chứa lara.sh (KHÔNG kèm SHA commit để luôn lấy bản mới nhất). Sau khi tạo gist, |
|
# sửa dòng này thành URL của bạn rồi cập nhật lại gist; hoặc ghi đè khi chạy bằng |
|
# biến môi trường: sudo LARA_UPDATE_URL="https://.../raw/lara.sh" lara update |
|
LARA_UPDATE_URL_DEFAULT="https://gist.githubusercontent.com/anhtuank7c/c2b5a523e2bf8bdcacad8b7c0e4856ac/raw/lara.sh" |
|
WWW_ROOT="/var/www" |
|
SOCK_DIR="/var/run/php" |
|
NGINX_AVAILABLE="/etc/nginx/sites-available" |
|
NGINX_ENABLED="/etc/nginx/sites-enabled" |
|
SUPPORTED_PHP=("7.4" "8.4") |
|
USER_PREFIX="lv_" |
|
|
|
# Tương thích Laravel <-> PHP (theo yêu cầu chính thức) |
|
# Laravel 8 : PHP 7.3 - 8.1 -> 7.4 |
|
# Laravel 9 : PHP 8.0 - 8.2 -> không có 7.4 lẫn 8.4 |
|
# Laravel 10 : PHP 8.1 - 8.3 -> không có 7.4 lẫn 8.4 |
|
# Laravel 11 : PHP 8.2 - 8.4 -> 8.4 |
|
# Laravel 12 : PHP 8.2 - 8.4 -> 8.4 |
|
# Laravel 13 : PHP >= 8.3 -> 8.4 |
|
compat_php_for_laravel() { |
|
case "$1" in |
|
8) echo "7.4" ;; |
|
11|12|13) echo "8.4" ;; |
|
9|10) echo "" ;; |
|
*) echo "" ;; |
|
esac |
|
} |
|
|
|
REQUIRED_EXTENSIONS=(mbstring xml curl bcmath ctype fileinfo pdo_mysql tokenizer zip gd intl) |
|
|
|
# Tên gói apt cho từng extension. Lưu ý: ctype/fileinfo/tokenizer được biên dịch |
|
# sẵn trong nhân PHP (gói php-common), KHÔNG có gói riêng; pdo_mysql nằm trong |
|
# gói php-mysql. Trả về đúng tên gói để hướng dẫn cài không bị sai. |
|
ext_package() { |
|
local v="$1" ext="$2" |
|
case "$ext" in |
|
ctype|fileinfo|tokenizer) echo "php$v-common" ;; |
|
pdo_mysql) echo "php$v-mysql" ;; |
|
*) echo "php$v-$ext" ;; |
|
esac |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# Hàm xuất thông báo |
|
#------------------------------------------------------------------------------- |
|
C_RED='\033[0;31m'; C_GRN='\033[0;32m'; C_YLW='\033[0;33m'; C_BLU='\033[0;34m'; C_RST='\033[0m' |
|
# Dùng %b cho phần màu (cần diễn giải \033) và %s cho nội dung (KHÔNG diễn giải |
|
# escape) để tránh chèn mã ANSI/giả mạo dòng log qua dữ liệu đầu vào. |
|
info() { printf '%b %s\n' "${C_BLU}[INFO]${C_RST}" "$*"; } |
|
ok() { printf '%b %s\n' "${C_GRN}[ OK ]${C_RST}" "$*"; } |
|
warn() { printf '%b %s\n' "${C_YLW}[CẢNH BÁO]${C_RST}" "$*"; } |
|
die() { printf '%b %s\n' "${C_RED}[LỖI]${C_RST}" "$*" >&2; exit 1; } |
|
|
|
usage() { |
|
sed -n '/^# Cách dùng:/,/^#====/p' "$0" | sed '$d' | sed 's/^# \{0,1\}//' |
|
exit "${1:-0}" |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# Kiểm tra dữ liệu đầu vào |
|
#------------------------------------------------------------------------------- |
|
require_root() { |
|
[[ $EUID -eq 0 ]] || die "Lệnh này cần quyền root. Hãy chạy lại với sudo." |
|
} |
|
|
|
validate_domain() { |
|
local d="$1" |
|
[[ "$d" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$ ]] \ |
|
|| die "Tên miền không hợp lệ: '$d' (ví dụ hợp lệ: example.com)" |
|
[[ ${#d} -le 253 ]] || die "Tên miền quá dài: $d" |
|
} |
|
|
|
validate_php_version() { |
|
local v="$1" s |
|
for s in "${SUPPORTED_PHP[@]}"; do [[ "$v" == "$s" ]] && return 0; done |
|
die "Phiên bản PHP '$v' không được hỗ trợ. Hỗ trợ: ${SUPPORTED_PHP[*]}" |
|
} |
|
|
|
validate_laravel_version() { |
|
local v="$1" |
|
[[ "$v" =~ ^(8|9|10|11|12|13)$ ]] || die "Phiên bản Laravel '$v' không được hỗ trợ. Hỗ trợ: 8-13" |
|
} |
|
|
|
# Chỉ chấp nhận URL git an toàn. Chặn: |
|
# - transport thực thi lệnh của git (ext::, fd::) => RCE |
|
# - file:// (đọc đường dẫn nội bộ) |
|
# - chuỗi bắt đầu bằng '-' (argument injection vào lệnh git) |
|
# - khoảng trắng / ký tự điều khiển |
|
validate_repo_url() { |
|
local url="$1" |
|
[[ -n "$url" ]] || die "URL git rỗng." |
|
[[ "$url" != -* ]] || die "URL git không được bắt đầu bằng '-': $url" |
|
[[ "$url" != *[[:space:][:cntrl:]]* ]] || die "URL git chứa khoảng trắng/ký tự điều khiển không hợp lệ." |
|
case "$url" in |
|
ext::*|fd::*|file://*) |
|
die "Transport git bị cấm vì lý do bảo mật: $url" ;; |
|
https://*|http://*|git://*|ssh://*) |
|
: ;; |
|
*@*:*) |
|
: ;; # dạng scp: user@host:đường-dẫn |
|
*) |
|
die "URL git không hợp lệ: $url |
|
Chỉ chấp nhận https://, http://, ssh://, git:// hoặc dạng user@host:path." ;; |
|
esac |
|
} |
|
|
|
validate_compat() { |
|
local laravel="$1" php="$2" required |
|
required="$(compat_php_for_laravel "$laravel")" |
|
if [[ -z "$required" ]]; then |
|
die "Laravel $laravel yêu cầu phiên bản PHP mà công cụ này không cung cấp. |
|
Laravel 9 cần PHP 8.0-8.2, Laravel 10 cần PHP 8.1-8.3, |
|
nhưng công cụ này chỉ hỗ trợ PHP ${SUPPORTED_PHP[*]}. |
|
Hãy chọn Laravel 8 (PHP 7.4) hoặc Laravel 11/12/13 (PHP 8.4)." |
|
fi |
|
[[ "$php" == "$required" ]] \ |
|
|| die "Laravel $laravel không tương thích với PHP $php. Hãy dùng PHP $required." |
|
} |
|
|
|
# Xác định user mà nginx worker đang chạy (thường là www-data) |
|
nginx_user() { |
|
local u |
|
u=$(grep -E '^\s*user\s+' /etc/nginx/nginx.conf 2>/dev/null \ |
|
| head -1 | awk '{print $2}' | tr -d ';') || true |
|
echo "${u:-www-data}" |
|
} |
|
|
|
check_environment() { |
|
local php_version="${1:-}" errors=0 |
|
|
|
info "Đang kiểm tra môi trường máy chủ..." |
|
|
|
# Hệ điều hành (hỗ trợ: Ubuntu 18.04 - 26.04 LTS) |
|
if [[ -f /etc/os-release ]]; then |
|
# shellcheck source=/dev/null |
|
. /etc/os-release |
|
if [[ "${ID:-}" == "ubuntu" ]]; then |
|
local os_major="${VERSION_ID%%.*}" |
|
if [[ "$os_major" =~ ^[0-9]+$ ]] && (( os_major >= 18 && os_major <= 26 )); then |
|
ok "Hệ điều hành: ${PRETTY_NAME:-Ubuntu ${VERSION_ID:-?}}" |
|
else |
|
warn "Ubuntu ${VERSION_ID:-?} nằm ngoài phạm vi hỗ trợ (18.04 - 26.04 LTS)" |
|
fi |
|
elif [[ "${ID:-}" == "debian" || "${ID_LIKE:-}" == *debian* ]]; then |
|
warn "Phát hiện hệ điều hành dạng Debian (${PRETTY_NAME:-không rõ}) - có thể chạy được, nhưng Ubuntu 18.04-26.04 mới là môi trường đã kiểm thử" |
|
else |
|
warn "Hệ điều hành chưa được kiểm thử: ${PRETTY_NAME:-không rõ} (công cụ này nhắm tới Ubuntu 18.04 - 26.04 LTS)" |
|
fi |
|
else |
|
warn "Không xác định được hệ điều hành (thiếu file /etc/os-release)" |
|
fi |
|
|
|
# nginx |
|
if command -v nginx >/dev/null 2>&1; then |
|
local ngx_ver |
|
ngx_ver=$(nginx -v 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) |
|
local ngx_major=${ngx_ver%%.*} ngx_minor; ngx_minor=$(echo "$ngx_ver" | cut -d. -f2) |
|
if (( ngx_major > 1 || (ngx_major == 1 && ngx_minor >= 18) )); then |
|
ok "nginx $ngx_ver" |
|
else |
|
warn "nginx $ngx_ver đã cũ (khuyến nghị >= 1.18)" |
|
fi |
|
[[ -d "$NGINX_AVAILABLE" && -d "$NGINX_ENABLED" ]] \ |
|
|| { warn "Thiếu cấu trúc thư mục $NGINX_AVAILABLE / $NGINX_ENABLED"; errors=$((errors + 1)); } |
|
grep -rqs "sites-enabled" /etc/nginx/nginx.conf \ |
|
|| warn "nginx.conf có vẻ chưa include thư mục sites-enabled/*" |
|
else |
|
warn "nginx chưa được cài đặt. Cài bằng lệnh: apt install nginx" |
|
errors=$((errors + 1)) |
|
fi |
|
|
|
# certbot + plugin nginx (cần cho 'lara cert' phát hành/gia hạn/thu hồi SSL) |
|
if command -v certbot >/dev/null 2>&1; then |
|
local cb_ver cb_plugins |
|
cb_ver=$(certbot --version 2>&1 | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1) |
|
# Lấy danh sách plugin một lần rồi đối chiếu bằng here-string - tránh đúng |
|
# lỗi SIGPIPE+pipefail như 'php -m | grep -q' (báo nhầm thiếu plugin nginx). |
|
cb_plugins=$(certbot plugins 2>/dev/null || true) |
|
if grep -qi 'nginx' <<<"$cb_plugins"; then |
|
ok "certbot ${cb_ver:-} (đã có plugin nginx)" |
|
else |
|
warn "certbot ${cb_ver:-} đã cài nhưng THIẾU plugin nginx ('lara cert' cần plugin này):" |
|
warn " Khắc phục: apt install python3-certbot-nginx" |
|
fi |
|
else |
|
warn "certbot chưa được cài (cần cho 'lara cert' / SSL): apt install certbot python3-certbot-nginx" |
|
fi |
|
|
|
# Phiên bản PHP |
|
local versions=("${SUPPORTED_PHP[@]}") |
|
[[ -n "$php_version" ]] && versions=("$php_version") |
|
local v |
|
for v in "${versions[@]}"; do |
|
if [[ -x "/usr/sbin/php-fpm$v" ]] && [[ -d "/etc/php/$v/fpm/pool.d" ]]; then |
|
ok "PHP $v FPM đã được cài đặt" |
|
# Cần trình CLI php$v để đọc danh sách extension. Nếu thiếu, mọi |
|
# extension sẽ bị báo "thiếu" oan -> kiểm tra và báo rõ trước. |
|
if [[ ! -x "/usr/bin/php$v" ]]; then |
|
warn "Thiếu trình CLI php$v nên không kiểm tra được extension." |
|
warn " Khắc phục: apt install php$v-cli" |
|
[[ -n "$php_version" ]] && errors=$((errors + 1)) |
|
continue |
|
fi |
|
# Lấy danh sách module MỘT lần vào biến rồi đối chiếu bằng here-string. |
|
# TUYỆT ĐỐI không dùng 'php -m | grep -q': grep -q thoát ngay khi khớp |
|
# -> php nhận SIGPIPE -> với 'set -o pipefail' pipeline trả non-zero |
|
# -> báo NHẦM là thiếu một cách ngẫu nhiên (theo thứ tự module). Bug thật. |
|
local installed_mods missing=() ext |
|
installed_mods=$("/usr/bin/php$v" -m 2>/dev/null) |
|
for ext in "${REQUIRED_EXTENSIONS[@]}"; do |
|
grep -qix "$ext" <<<"$installed_mods" || missing+=("$ext") |
|
done |
|
if ((${#missing[@]})); then |
|
warn "PHP $v thiếu các extension: ${missing[*]}" |
|
# Quy đổi sang tên gói đúng và bỏ trùng (nhiều extension có thể |
|
# cùng thuộc php-common). |
|
local pkgs=() fix |
|
for ext in "${missing[@]}"; do pkgs+=("$(ext_package "$v" "$ext")"); done |
|
fix=$(printf '%s\n' "${pkgs[@]}" | sort -u | tr '\n' ' ') |
|
warn " Khắc phục: apt install ${fix% }" |
|
[[ -n "$php_version" ]] && errors=$((errors + 1)) |
|
else |
|
ok "PHP $v đã có đủ các extension cần thiết" |
|
fi |
|
else |
|
warn "PHP $v FPM chưa được cài đặt." |
|
warn " Khắc phục: add-apt-repository ppa:ondrej/php && apt update" |
|
warn " apt install php$v-fpm php$v-cli $(printf "php$v-%s " mbstring xml curl bcmath mysql zip gd intl)" |
|
[[ -n "$php_version" ]] && errors=$((errors + 1)) |
|
fi |
|
done |
|
|
|
# composer |
|
if command -v composer >/dev/null 2>&1; then |
|
ok "composer $(composer --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" |
|
else |
|
warn "composer chưa được cài đặt." |
|
warn " Khắc phục: curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer" |
|
errors=$((errors + 1)) |
|
fi |
|
|
|
# git (chỉ cần cho --repo / deploy) |
|
command -v git >/dev/null 2>&1 || warn "git chưa được cài (cần khi deploy bằng --repo): apt install git" |
|
|
|
# MySQL/MariaDB (không chặn - có thể dùng CSDL từ xa hoặc SQLite) |
|
local db_ver="" |
|
if systemctl is-active --quiet mariadb 2>/dev/null; then |
|
db_ver=$(mysql --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) || true |
|
ok "MariaDB ${db_ver:-} đang chạy" |
|
elif systemctl is-active --quiet mysql 2>/dev/null; then |
|
db_ver=$(mysql --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1) || true |
|
ok "MySQL ${db_ver:-} đang chạy" |
|
elif command -v mysqld >/dev/null 2>&1 || command -v mariadbd >/dev/null 2>&1; then |
|
warn "MySQL/MariaDB đã cài nhưng chưa chạy: systemctl start mariadb (hoặc mysql)" |
|
else |
|
warn "MySQL/MariaDB chưa cài (bỏ qua nếu dùng CSDL từ xa/SQLite): apt install mariadb-server" |
|
fi |
|
|
|
# Redis (không chặn - chỉ cần khi dùng cache/queue/session Redis) |
|
if systemctl is-active --quiet redis-server 2>/dev/null || systemctl is-active --quiet redis 2>/dev/null; then |
|
ok "Redis đang chạy" |
|
elif command -v redis-server >/dev/null 2>&1; then |
|
warn "Redis đã cài nhưng chưa chạy: systemctl start redis-server" |
|
else |
|
warn "Redis chưa cài (bỏ qua nếu không dùng cache/queue Redis): apt install redis-server" |
|
fi |
|
|
|
if ((errors > 0)); then |
|
die "Kiểm tra môi trường thất bại với $errors lỗi nghiêm trọng. Hãy khắc phục các mục trên rồi thử lại." |
|
fi |
|
ok "Môi trường đạt yêu cầu." |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# Hàm đặt tên |
|
#------------------------------------------------------------------------------- |
|
site_user_for() { |
|
# example.com -> lv_example_com (username Linux: tối đa 32 ký tự, không có dấu chấm) |
|
local slug |
|
slug=$(echo "$1" | tr 'A-Z.-' 'a-z__' | tr -cd 'a-z0-9_') |
|
echo "${USER_PREFIX}${slug}" | cut -c1-32 |
|
} |
|
|
|
sock_path_for() { echo "$SOCK_DIR/${2}_${1}.sock"; } # $1=domain $2=php |
|
pool_conf_for() { echo "/etc/php/$2/fpm/pool.d/$1.conf"; } |
|
nginx_conf_for() { echo "$NGINX_AVAILABLE/$1.conf"; } |
|
|
|
# Tìm phiên bản PHP của một domain dựa trên file pool |
|
find_php_version() { |
|
local domain="$1" v |
|
for v in "${SUPPORTED_PHP[@]}"; do |
|
[[ -f "$(pool_conf_for "$domain" "$v")" ]] && { echo "$v"; return 0; } |
|
done |
|
return 1 |
|
} |
|
|
|
# Thực thi một lệnh THAY ĐỔI hệ thống - ở chế độ dry-run thì chỉ in ra. |
|
# Các lệnh chỉ đọc (kiểm tra, validate) không đi qua hàm này. |
|
run_cmd() { |
|
if [[ -n "$DRY_RUN" ]]; then |
|
echo -e "${C_YLW}[DRY-RUN]${C_RST} $*" |
|
else |
|
"$@" |
|
fi |
|
} |
|
|
|
# Ghi file từ stdin - ở chế độ dry-run thì in nội dung sẽ ghi. |
|
write_file() { |
|
local path="$1" |
|
if [[ -n "$DRY_RUN" ]]; then |
|
echo -e "${C_YLW}[DRY-RUN]${C_RST} Sẽ ghi file $path với nội dung:" |
|
sed 's/^/ | /' |
|
else |
|
cat > "$path" |
|
fi |
|
} |
|
|
|
run_as() { |
|
local user="$1"; shift |
|
if [[ -n "$DRY_RUN" ]]; then |
|
echo -e "${C_YLW}[DRY-RUN]${C_RST} (chạy bằng user $user) $*" |
|
return 0 |
|
fi |
|
sudo -u "$user" -H env COMPOSER_ALLOW_SUPERUSER=0 "$@" |
|
} |
|
|
|
# Khoá theo từng domain để hai tiến trình lara không thao tác đồng thời lên cùng |
|
# một website (tránh đua tranh git/composer/migrate). Khoá tự nhả khi thoát. |
|
acquire_lock() { |
|
local domain="$1" |
|
[[ -n "$DRY_RUN" ]] && return 0 |
|
command -v flock >/dev/null 2>&1 || { warn "Không có 'flock' - bỏ qua khoá chống chạy song song."; return 0; } |
|
local lock_dir="/run/lock" |
|
[[ -d "$lock_dir" && -w "$lock_dir" ]] || lock_dir="/tmp" |
|
exec 9>"$lock_dir/lara-$domain.lock" 2>/dev/null || return 0 |
|
flock -n 9 \ |
|
|| die "Đang có một tiến trình lara khác thao tác trên '$domain'. Hãy đợi nó hoàn tất rồi thử lại." |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# Database (tuỳ chọn) - mỗi website một database + user CSDL riêng |
|
#------------------------------------------------------------------------------- |
|
db_service_running() { |
|
systemctl is-active --quiet mariadb 2>/dev/null \ |
|
|| systemctl is-active --quiet mysql 2>/dev/null |
|
} |
|
|
|
mysql_client() { |
|
if command -v mysql >/dev/null 2>&1; then mysql "$@" |
|
elif command -v mariadb >/dev/null 2>&1; then mariadb "$@" |
|
else die "Không tìm thấy mysql/mariadb client." |
|
fi |
|
} |
|
|
|
db_exists() { |
|
[[ -n "$(mysql_client -N -e \ |
|
"SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME='$1'" 2>/dev/null)" ]] |
|
} |
|
|
|
# Tạo database + user CSDL cùng tên slug của website, mật khẩu ngẫu nhiên. |
|
# Kết quả đặt vào các biến toàn cục DB_NAME / DB_USER / DB_PASS. |
|
create_site_database() { |
|
local domain="$1" |
|
DB_NAME=$(site_user_for "$domain") |
|
DB_USER="$DB_NAME" |
|
|
|
db_service_running \ |
|
|| die "MySQL/MariaDB chưa chạy - không thể tạo database. Hãy khởi động dịch vụ (systemctl start mariadb) hoặc bỏ tuỳ chọn --db." |
|
# Kiểm tra kết nối quyền quản trị trước. Ubuntu mặc định cho root kết nối |
|
# qua unix_socket khi chạy bằng sudo; nếu root CSDL có mật khẩu thì cần |
|
# /root/.my.cnf. Báo lỗi rõ ràng thay vì để lệnh CREATE thất bại khó hiểu. |
|
if [[ -z "$DRY_RUN" ]] && ! mysql_client -e "SELECT 1;" >/dev/null 2>&1; then |
|
die "Không kết nối được MySQL/MariaDB bằng quyền quản trị. |
|
Ubuntu mặc định cho phép root kết nối qua unix_socket (chạy lara bằng sudo). |
|
Nếu root CSDL có mật khẩu: tạo file /root/.my.cnf với mục [client] user+password, |
|
hoặc tự tạo database thủ công rồi tạo website mà KHÔNG dùng --db." |
|
fi |
|
db_exists "$DB_NAME" \ |
|
&& die "Database '$DB_NAME' đã tồn tại - không ghi đè. Hãy tự xử lý database này hoặc bỏ tuỳ chọn --db." |
|
|
|
DB_PASS=$(openssl rand -hex 16 2>/dev/null) \ |
|
|| DB_PASS=$(head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n') |
|
|
|
if [[ -n "$DRY_RUN" ]]; then |
|
echo -e "${C_YLW}[DRY-RUN]${C_RST} Sẽ chạy SQL:" |
|
echo " | CREATE DATABASE \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" |
|
echo " | CREATE USER '$DB_USER'@'localhost' IDENTIFIED BY '<mật khẩu ngẫu nhiên>';" |
|
echo " | GRANT ALL PRIVILEGES ON \`$DB_NAME\`.* TO '$DB_USER'@'localhost';" |
|
return 0 |
|
fi |
|
mysql_client <<SQL |
|
CREATE DATABASE \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; |
|
CREATE USER '$DB_USER'@'localhost' IDENTIFIED BY '$DB_PASS'; |
|
GRANT ALL PRIVILEGES ON \`$DB_NAME\`.* TO '$DB_USER'@'localhost'; |
|
FLUSH PRIVILEGES; |
|
SQL |
|
ok "Đã tạo database '$DB_NAME' và user CSDL '$DB_USER'@'localhost'" |
|
} |
|
|
|
# Ghi thông tin CSDL vào file .env (xử lý được cả dòng đang bị chú thích '# DB_...') |
|
write_env_db() { |
|
local site_user="$1" env_file="$2" |
|
[[ -f "$env_file" ]] || return 0 |
|
run_as "$site_user" sed -i -E \ |
|
-e "s|^#? ?DB_CONNECTION=.*|DB_CONNECTION=mysql|" \ |
|
-e "s|^#? ?DB_HOST=.*|DB_HOST=127.0.0.1|" \ |
|
-e "s|^#? ?DB_PORT=.*|DB_PORT=3306|" \ |
|
-e "s|^#? ?DB_DATABASE=.*|DB_DATABASE=$DB_NAME|" \ |
|
-e "s|^#? ?DB_USERNAME=.*|DB_USERNAME=$DB_USER|" \ |
|
-e "s|^#? ?DB_PASSWORD=.*|DB_PASSWORD=$DB_PASS|" \ |
|
"$env_file" |
|
ok "Đã ghi thông tin CSDL vào $env_file" |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# Áp bộ quyền chuẩn cho một website (dùng chung cho 'create' và 'fixperms'): |
|
# - chủ sở hữu = user riêng của site, group = user nginx (www-data) |
|
# - thư mục 750, file 640; người ngoài không có quyền |
|
# - storage/ và bootstrap/cache ghi được (owner + group) |
|
# - artisan thực thi được (750); .env siết 600 (chứa bí mật) |
|
# - đảm bảo có storage/tmp (thư mục tạm riêng theo cấu hình pool) |
|
#------------------------------------------------------------------------------- |
|
apply_site_permissions() { |
|
local web_root="$1" site_user="$2" ngx_user="$3" d |
|
run_cmd mkdir -p "$web_root/storage/tmp" |
|
run_cmd chown -R "$site_user:$ngx_user" "$web_root" |
|
run_cmd find "$web_root" -type d -exec chmod 750 {} + |
|
run_cmd find "$web_root" -type f -exec chmod 640 {} + |
|
for d in "$web_root/storage" "$web_root/bootstrap/cache"; do |
|
[[ -d "$d" ]] && run_cmd chmod -R u+rwX,g+rwX "$d" |
|
done |
|
run_cmd chmod 750 "$web_root" |
|
[[ -f "$web_root/artisan" ]] && run_cmd chmod 750 "$web_root/artisan" |
|
[[ -f "$web_root/.env" ]] && run_cmd chmod 600 "$web_root/.env" |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# Hoàn tác khi 'create' thất bại giữa chừng. Chỉ chạy khi RB_ACTIVE đang bật |
|
# (tức đã qua assert_site_absent -> chắc chắn mọi tài nguyên vốn KHÔNG tồn tại, |
|
# nên xoá đi là an toàn) và lệnh thoát với mã lỗi. An toàn để gọi nhiều lần. |
|
#------------------------------------------------------------------------------- |
|
_create_rollback() { |
|
local code=$? |
|
[[ -n "${RB_ACTIVE:-}" ]] || return 0 |
|
[[ -n "$DRY_RUN" ]] && return 0 |
|
(( code == 0 )) && return 0 |
|
|
|
warn "Tạo website thất bại - đang hoàn tác các thay đổi đã thực hiện..." |
|
rm -f "$NGINX_ENABLED/$RB_DOMAIN.conf" "$(nginx_conf_for "$RB_DOMAIN")" 2>/dev/null || true |
|
local v |
|
for v in "${SUPPORTED_PHP[@]}"; do |
|
rm -f "$(pool_conf_for "$RB_DOMAIN" "$v")" "$(sock_path_for "$RB_DOMAIN" "$v")" 2>/dev/null || true |
|
done |
|
[[ -n "$RB_PHP" ]] && systemctl restart "php$RB_PHP-fpm" 2>/dev/null || true |
|
nginx -t >/dev/null 2>&1 && systemctl reload nginx 2>/dev/null || true |
|
[[ -d "$WWW_ROOT/$RB_DOMAIN" ]] && { rm -rf "${WWW_ROOT:?}/$RB_DOMAIN" || true; } |
|
if id "$RB_USER" &>/dev/null; then |
|
userdel -r "$RB_USER" 2>/dev/null || userdel "$RB_USER" 2>/dev/null || true |
|
fi |
|
if [[ -n "$RB_DB" ]] && db_service_running && db_exists "$RB_USER"; then |
|
mysql_client -e "DROP DATABASE IF EXISTS \`$RB_USER\`; DROP USER IF EXISTS '$RB_USER'@'localhost'; FLUSH PRIVILEGES;" 2>/dev/null || true |
|
fi |
|
warn "Đã hoàn tác. Hệ thống trở lại trạng thái trước khi tạo '$RB_DOMAIN'." |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# Chống tạo trùng: kiểm tra TẤT CẢ tài nguyên của website TRƯỚC khi tạo bất cứ |
|
# thứ gì. Chỉ cần một thành phần đã tồn tại là dừng ngay và liệt kê, để không |
|
# bao giờ tạo website trùng hoặc chồng lên một website đang có. |
|
#------------------------------------------------------------------------------- |
|
assert_site_absent() { |
|
local domain="$1" site_user="$2" want_db="$3" |
|
local conflicts=() v |
|
|
|
[[ -e "$WWW_ROOT/$domain" ]] && conflicts+=("thư mục mã nguồn: $WWW_ROOT/$domain") |
|
[[ -e "$(nginx_conf_for "$domain")" ]] && conflicts+=("cấu hình nginx: $(nginx_conf_for "$domain")") |
|
# -e theo symlink (link gãy sẽ trả false), nên kiểm tra thêm -L để bắt cả |
|
# symlink trỏ tới đích đã bị xoá - đúng kiểu rác mà site xoá dở để lại. |
|
{ [[ -e "$NGINX_ENABLED/$domain.conf" ]] || [[ -L "$NGINX_ENABLED/$domain.conf" ]]; } \ |
|
&& conflicts+=("symlink nginx: $NGINX_ENABLED/$domain.conf") |
|
for v in "${SUPPORTED_PHP[@]}"; do |
|
[[ -e "$(pool_conf_for "$domain" "$v")" ]] && conflicts+=("pool PHP $v: $(pool_conf_for "$domain" "$v")") |
|
[[ -S "$(sock_path_for "$domain" "$v")" ]] && conflicts+=("socket PHP $v: $(sock_path_for "$domain" "$v")") |
|
done |
|
id "$site_user" &>/dev/null && conflicts+=("user hệ thống: $site_user") |
|
if [[ -n "$want_db" ]] && db_service_running && db_exists "$site_user"; then |
|
conflicts+=("database: $site_user") |
|
fi |
|
|
|
if (( ${#conflicts[@]} > 0 )); then |
|
local msg="Website '$domain' (hoặc tài nguyên cùng tên) ĐÃ TỒN TẠI - không tạo trùng." |
|
msg+=$'\n'" Các thành phần đã có:" |
|
local c |
|
for c in "${conflicts[@]}"; do msg+=$'\n'" - $c"; done |
|
msg+=$'\n'" Muốn tạo lại? Hãy xoá website trước bằng: $0 remove $domain" |
|
die "$msg" |
|
fi |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# create - tạo website mới |
|
#------------------------------------------------------------------------------- |
|
cmd_create() { |
|
local domain="" php_version="" laravel_version="" repo_url="" want_db="" |
|
DB_NAME=""; DB_USER=""; DB_PASS="" |
|
|
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
--php) php_version="${2:-}"; shift 2 ;; |
|
--laravel) laravel_version="${2:-}"; shift 2 ;; |
|
--repo) repo_url="${2:-}"; shift 2 ;; |
|
--db) want_db=1; shift ;; |
|
-h|--help) usage ;; |
|
-*) die "Tham số không hợp lệ: $1" ;; |
|
*) if [[ -z "$domain" ]]; then domain="$1"; else die "Tham số thừa: $1"; fi; shift ;; |
|
esac |
|
done |
|
|
|
[[ -n "$domain" ]] || die "Thiếu tên miền. Cách dùng: $0 create <domain> --php <7.4|8.4> --laravel <8-13>" |
|
validate_domain "$domain" |
|
|
|
# Hỏi tương tác nếu thiếu tham số |
|
if [[ -z "$php_version" ]]; then |
|
read -rp "Phiên bản PHP [${SUPPORTED_PHP[*]}]: " php_version |
|
fi |
|
validate_php_version "$php_version" |
|
|
|
if [[ -z "$laravel_version" ]]; then |
|
read -rp "Phiên bản Laravel [8-13]: " laravel_version |
|
fi |
|
validate_laravel_version "$laravel_version" |
|
validate_compat "$laravel_version" "$php_version" |
|
[[ -n "$repo_url" ]] && validate_repo_url "$repo_url" |
|
|
|
local web_root="$WWW_ROOT/$domain" |
|
local site_user; site_user=$(site_user_for "$domain") |
|
local sock; sock=$(sock_path_for "$domain" "$php_version") |
|
local pool_conf; pool_conf=$(pool_conf_for "$domain" "$php_version") |
|
local ngx_conf; ngx_conf=$(nginx_conf_for "$domain") |
|
local ngx_user; ngx_user=$(nginx_user) |
|
|
|
# Chống tạo trùng: kiểm tra mọi tài nguyên TRƯỚC khi tạo bất cứ thứ gì. |
|
assert_site_absent "$domain" "$site_user" "$want_db" |
|
|
|
check_environment "$php_version" |
|
|
|
# Khoá domain (chống chạy song song) và bật cơ chế hoàn tác: kể từ đây, nếu |
|
# có lỗi giữa chừng, mọi thứ vừa tạo sẽ được dọn sạch (xem _create_rollback). |
|
acquire_lock "$domain" |
|
RB_DOMAIN="$domain"; RB_USER="$site_user"; RB_PHP="$php_version"; RB_DB="$want_db"; RB_ACTIVE=1 |
|
trap _create_rollback EXIT |
|
|
|
# Tạo database trước tiên (nếu được yêu cầu) - lỗi ở bước này sẽ dừng sớm, |
|
# trước khi bất kỳ thành phần nào của website được tạo ra. |
|
if [[ -n "$want_db" ]]; then |
|
create_site_database "$domain" |
|
fi |
|
|
|
info "Đang tạo website $domain (PHP $php_version, Laravel $laravel_version, user $site_user)" |
|
|
|
#--- user hệ thống riêng, thuộc group của nginx ----------------------------- |
|
# assert_site_absent đã đảm bảo user chưa tồn tại. Nếu nó vừa xuất hiện |
|
# (đua tranh hiếm gặp, hoặc trùng slug với site khác) thì DỪNG thay vì dùng |
|
# lại - dùng nhầm user của site khác sẽ phá vỡ sự cô lập giữa các site. |
|
id "$site_user" &>/dev/null \ |
|
&& die "User $site_user vừa xuất hiện (xung đột tên/đua tranh) - đã dừng để an toàn." |
|
run_cmd useradd --system --create-home --home-dir "/home/$site_user" \ |
|
--shell /usr/sbin/nologin "$site_user" |
|
ok "Đã tạo user hệ thống $site_user" |
|
run_cmd usermod -aG "$ngx_user" "$site_user" |
|
|
|
#--- thư mục mã nguồn + cài Laravel ------------------------------------------ |
|
run_cmd mkdir -p "$web_root" |
|
run_cmd chown "$site_user:$ngx_user" "$web_root" |
|
|
|
if [[ -n "$repo_url" ]]; then |
|
info "Đang clone $repo_url ..." |
|
run_as "$site_user" git clone "$repo_url" "$web_root" |
|
info "Đang cài các gói composer..." |
|
run_as "$site_user" composer install --working-dir="$web_root" \ |
|
--no-dev --optimize-autoloader --no-interaction |
|
if [[ ! -f "$web_root/.env" ]]; then |
|
run_as "$site_user" cp "$web_root/.env.example" "$web_root/.env" |
|
run_as "$site_user" "php$php_version" "$web_root/artisan" key:generate --force |
|
fi |
|
else |
|
info "Tạo thư mục trống (bạn sẽ tự đưa mã nguồn lên sau)..." |
|
run_as "$site_user" mkdir -p "$web_root/public" |
|
write_file "$web_root/public/index.php" <<EOF |
|
<?php |
|
// File tạm do lara tạo - hãy thay bằng mã nguồn Laravel của bạn. |
|
echo 'Website ' . htmlspecialchars(\$_SERVER['HTTP_HOST'] ?? '$domain') . ' đang được thiết lập.'; |
|
EOF |
|
run_cmd chown "$site_user:$ngx_user" "$web_root/public/index.php" |
|
fi |
|
|
|
# APP_URL (dùng http cho tới khi chạy certbot) |
|
[[ -f "$web_root/.env" ]] && run_as "$site_user" \ |
|
sed -i "s|^APP_URL=.*|APP_URL=http://$domain|" "$web_root/.env" |
|
|
|
# Ghi thông tin CSDL vào .env (nếu đã tạo database và có file .env) |
|
if [[ -n "$want_db" ]]; then |
|
write_env_db "$site_user" "$web_root/.env" |
|
fi |
|
|
|
#--- phân quyền (dùng chung với 'lara fixperms') ---------------------------- |
|
# storage/tmp + chủ sở hữu site_user:nginx, 750/640, storage ghi được, .env 600 |
|
apply_site_permissions "$web_root" "$site_user" "$ngx_user" |
|
ok "Đã phân quyền ($site_user:$ngx_user, 750/640, .env 600, cho phép ghi vào storage)" |
|
|
|
#--- pool php-fpm -------------------------------------------------------------- |
|
write_file "$pool_conf" <<EOF |
|
; Managed by lara - site: $domain |
|
[$domain] |
|
user = $site_user |
|
group = $site_user |
|
|
|
listen = $sock |
|
listen.owner = $ngx_user |
|
listen.group = $ngx_user |
|
listen.mode = 0660 |
|
|
|
pm = dynamic |
|
pm.max_children = 10 |
|
pm.start_servers = 2 |
|
pm.min_spare_servers = 1 |
|
pm.max_spare_servers = 4 |
|
pm.max_requests = 500 |
|
|
|
chdir = $web_root |
|
|
|
php_admin_value[error_log] = /var/log/php/${php_version}_${domain}.error.log |
|
php_admin_flag[log_errors] = on |
|
php_admin_value[memory_limit] = 256M |
|
php_admin_value[upload_max_filesize] = 64M |
|
php_admin_value[post_max_size] = 64M |
|
php_admin_value[open_basedir] = $web_root:/usr/share/php |
|
php_admin_value[sys_temp_dir] = $web_root/storage/tmp |
|
php_admin_value[upload_tmp_dir] = $web_root/storage/tmp |
|
php_admin_value[session.save_path] = $web_root/storage/framework/sessions |
|
EOF |
|
run_cmd mkdir -p /var/log/php |
|
run_cmd "php-fpm$php_version" -t || die "Kiểm tra cấu hình php-fpm$php_version thất bại (xem chi tiết phía trên)." |
|
run_cmd systemctl restart "php$php_version-fpm" |
|
run_cmd systemctl enable -q "php$php_version-fpm" 2>/dev/null || true |
|
ok "Đã tạo pool PHP-FPM -> $sock" |
|
|
|
#--- vhost nginx (chỉ HTTP; certbot sẽ thêm SSL sau) -------------------------- |
|
write_file "$ngx_conf" <<EOF |
|
# Managed by lara - site: $domain |
|
# Chỉ HTTP. Chạy certbot để thêm SSL (certbot sẽ tự sửa file này). |
|
server { |
|
listen 80; |
|
listen [::]:80; |
|
server_name $domain www.$domain; |
|
root $web_root/public; |
|
|
|
index index.php; |
|
charset utf-8; |
|
|
|
access_log /var/log/nginx/${domain}.access.log; |
|
error_log /var/log/nginx/${domain}.error.log; |
|
|
|
client_max_body_size 64M; |
|
|
|
add_header X-Frame-Options "SAMEORIGIN"; |
|
add_header X-Content-Type-Options "nosniff"; |
|
|
|
location / { |
|
try_files \$uri \$uri/ /index.php?\$query_string; |
|
} |
|
|
|
location = /favicon.ico { access_log off; log_not_found off; } |
|
location = /robots.txt { access_log off; log_not_found off; } |
|
|
|
location ~ \.php\$ { |
|
try_files \$uri =404; |
|
fastcgi_pass unix:$sock; |
|
fastcgi_index index.php; |
|
fastcgi_param SCRIPT_FILENAME \$realpath_root\$fastcgi_script_name; |
|
include fastcgi_params; |
|
fastcgi_hide_header X-Powered-By; |
|
} |
|
|
|
location ~ /\.(?!well-known).* { |
|
deny all; |
|
} |
|
} |
|
EOF |
|
run_cmd ln -sf "$ngx_conf" "$NGINX_ENABLED/$domain.conf" |
|
run_cmd nginx -t || die "Kiểm tra cấu hình nginx thất bại. Website CHƯA được kích hoạt. Hãy xem lại $ngx_conf" |
|
run_cmd systemctl reload nginx |
|
ok "Đã kích hoạt vhost nginx (chỉ HTTP)" |
|
|
|
# Mọi bước đã xong - tắt cơ chế hoàn tác để website được giữ lại. |
|
RB_ACTIVE=""; trap - EXIT |
|
|
|
#--- tóm tắt ------------------------------------------------------------------- |
|
echo |
|
echo "===============================================================" |
|
ok "Tạo website thành công!" |
|
echo " Tên miền : http://$domain" |
|
echo " Mã nguồn : $web_root" |
|
echo " Chạy bằng : $site_user (thuộc group $ngx_user)" |
|
echo " PHP : $php_version (socket: $sock)" |
|
echo " Laravel : $laravel_version" |
|
echo " Pool FPM : $pool_conf" |
|
echo " Cấu hình nginx : $ngx_conf" |
|
if [[ -n "$DB_NAME" ]]; then |
|
echo |
|
echo " Database : $DB_NAME (CHARACTER SET utf8mb4)" |
|
echo " User CSDL : $DB_USER@localhost" |
|
if [[ -f "$web_root/.env" ]]; then |
|
# Mật khẩu đã nằm an toàn trong .env (quyền 600) - không in ra |
|
# màn hình để tránh lọt vào scrollback/log terminal/CI. |
|
echo " Mật khẩu : đã ghi vào $web_root/.env (quyền 600), không in ra màn hình" |
|
else |
|
echo -e " Mật khẩu : ${C_YLW}$DB_PASS${C_RST}" |
|
echo -e " ${C_YLW}HÃY LƯU MẬT KHẨU NÀY NGAY${C_RST} - nó chỉ hiển thị một lần duy nhất," |
|
echo " bạn sẽ cần điền vào .env khi đưa mã nguồn lên." |
|
fi |
|
fi |
|
echo |
|
echo " Các bước tiếp theo:" |
|
echo " 1. Trỏ bản ghi DNS A của $domain (và www.$domain) về máy chủ này." |
|
if [[ -n "$repo_url" ]]; then |
|
echo " 2. Cấu hình cơ sở dữ liệu trong $web_root/.env, sau đó chạy:" |
|
echo " sudo -u $site_user php$php_version $web_root/artisan migrate" |
|
else |
|
echo " 2. Đưa mã nguồn Laravel vào $web_root (đè lên public/index.php tạm)," |
|
echo " sau đó cấp lại quyền sở hữu và chạy migrate:" |
|
echo " sudo chown -R $site_user:$ngx_user $web_root" |
|
echo " sudo -u $site_user composer install --working-dir=$web_root --no-dev" |
|
echo " sudo -u $site_user php$php_version $web_root/artisan migrate" |
|
fi |
|
echo " 3. Kích hoạt SSL (sau khi DNS đã trỏ đúng) bằng một trong hai cách:" |
|
echo |
|
echo -e " ${C_GRN}sudo lara cert issue $domain${C_RST}" |
|
echo -e " ${C_GRN}sudo certbot --nginx -d $domain -d www.$domain${C_RST}" |
|
echo |
|
echo " (Nếu www.$domain chưa có bản ghi DNS: dùng 'lara cert issue $domain --no-www'.)" |
|
echo "===============================================================" |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# Đưa site ra khỏi chế độ bảo trì khi 'deploy' thoát - kể cả khi lỗi giữa chừng - |
|
# để site không bao giờ bị kẹt ở trang 503 "đang bảo trì". |
|
#------------------------------------------------------------------------------- |
|
_deploy_bringup() { |
|
[[ -n "${DEP_ACTIVE:-}" ]] || return 0 |
|
[[ -n "$DRY_RUN" ]] && return 0 |
|
sudo -u "$DEP_USER" -H "php$DEP_PHP" "$WWW_ROOT/$DEP_DOMAIN/artisan" up >/dev/null 2>&1 || true |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# deploy - cập nhật website đang chạy (git pull + composer + migrate + cache) |
|
#------------------------------------------------------------------------------- |
|
cmd_deploy() { |
|
local domain="${1:-}" |
|
[[ -n "$domain" ]] || die "Cách dùng: $0 deploy <domain>" |
|
validate_domain "$domain" |
|
acquire_lock "$domain" |
|
|
|
local web_root="$WWW_ROOT/$domain" |
|
[[ -d "$web_root" ]] || die "Không tìm thấy website: $web_root" |
|
|
|
local php_version |
|
php_version=$(find_php_version "$domain") || die "Không tìm thấy pool php-fpm nào của $domain." |
|
local site_user; site_user=$(site_user_for "$domain") |
|
local artisan=("php$php_version" "$web_root/artisan") |
|
|
|
info "Đang deploy $domain (PHP $php_version, user $site_user)" |
|
|
|
run_as "$site_user" "${artisan[@]}" down || warn "Không bật được chế độ bảo trì (vẫn tiếp tục)." |
|
# Site đã vào bảo trì -> bật lưới an toàn: dù lỗi ở bước nào phía dưới, khi |
|
# tiến trình thoát site vẫn được đưa trở lại online. |
|
DEP_DOMAIN="$domain"; DEP_USER="$site_user"; DEP_PHP="$php_version"; DEP_ACTIVE=1 |
|
trap _deploy_bringup EXIT |
|
|
|
if [[ -d "$web_root/.git" ]]; then |
|
info "Đang kéo mã nguồn mới nhất..." |
|
run_as "$site_user" git -C "$web_root" pull --ff-only |
|
else |
|
warn "$web_root không phải là git repository - bỏ qua bước kéo mã nguồn." |
|
fi |
|
|
|
info "Đang cài các gói composer..." |
|
run_as "$site_user" composer install --working-dir="$web_root" \ |
|
--no-dev --optimize-autoloader --no-interaction |
|
|
|
info "Đang chạy migration và tạo lại cache..." |
|
run_as "$site_user" "${artisan[@]}" migrate --force |
|
run_as "$site_user" "${artisan[@]}" config:cache |
|
run_as "$site_user" "${artisan[@]}" route:cache |
|
run_as "$site_user" "${artisan[@]}" view:cache |
|
|
|
run_as "$site_user" "${artisan[@]}" up |
|
run_cmd systemctl reload "php$php_version-fpm" |
|
# Thành công - đã 'up' tường minh ở trên, tắt lưới an toàn. |
|
DEP_ACTIVE=""; trap - EXIT |
|
ok "Đã deploy $domain" |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# fixperms - sửa lại quyền cho một website. Hữu ích sau khi upload code qua SFTP |
|
# bằng tài khoản root (làm file thành root:root) khiến PHP-FPM mất quyền ghi |
|
# storage/ và nginx mất quyền đọc. Áp đúng bộ quyền mà 'create' đặt ban đầu. |
|
#------------------------------------------------------------------------------- |
|
cmd_fixperms() { |
|
local domain="${1:-}" |
|
[[ -n "$domain" ]] || die "Cách dùng: $0 fixperms <domain>" |
|
validate_domain "$domain" |
|
acquire_lock "$domain" |
|
|
|
local web_root="$WWW_ROOT/$domain" |
|
[[ -d "$web_root" ]] || die "Không tìm thấy website: $web_root" |
|
local site_user; site_user=$(site_user_for "$domain") |
|
id "$site_user" &>/dev/null \ |
|
|| die "Không thấy user hệ thống '$site_user' của website này (website này có do lara tạo không?)." |
|
local ngx_user; ngx_user=$(nginx_user) |
|
local php_version; php_version=$(find_php_version "$domain") || php_version="<ver>" |
|
|
|
info "Đang sửa quyền cho $domain (chủ sở hữu $site_user, group $ngx_user)..." |
|
# Đảm bảo user của site vẫn thuộc group nginx để www-data đọc được tài nguyên tĩnh. |
|
run_cmd usermod -aG "$ngx_user" "$site_user" |
|
apply_site_permissions "$web_root" "$site_user" "$ngx_user" |
|
ok "Đã sửa quyền cho $domain ($site_user:$ngx_user, 750/640, storage ghi được, .env 600)." |
|
info "Nếu app vẫn lỗi, xoá cache rồi nạp lại:" |
|
info " sudo -u $site_user php$php_version $web_root/artisan optimize:clear" |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# remove - xoá website |
|
#------------------------------------------------------------------------------- |
|
cmd_remove() { |
|
local domain="${1:-}" |
|
[[ -n "$domain" ]] || die "Cách dùng: $0 remove <domain>" |
|
validate_domain "$domain" |
|
acquire_lock "$domain" |
|
|
|
local web_root="$WWW_ROOT/$domain" |
|
local site_user; site_user=$(site_user_for "$domain") |
|
local php_version="" |
|
php_version=$(find_php_version "$domain") || true |
|
|
|
echo "Thao tác này sẽ xoá website '$domain':" |
|
echo " - Cấu hình nginx + symlink" |
|
[[ -n "$php_version" ]] && echo " - Pool FPM PHP $php_version" |
|
echo " - (tuỳ chọn) thư mục mã nguồn $web_root và user $site_user" |
|
read -rp "Tiếp tục? [y/N] " answer |
|
[[ "$answer" =~ ^[Yy]$ ]] || die "Đã huỷ." |
|
|
|
run_cmd rm -f "$NGINX_ENABLED/$domain.conf" "$(nginx_conf_for "$domain")" |
|
if run_cmd nginx -t; then |
|
run_cmd systemctl reload nginx |
|
else |
|
warn "Kiểm tra cấu hình nginx thất bại - hãy kiểm tra thủ công các cấu hình còn lại." |
|
fi |
|
ok "Đã xoá cấu hình nginx" |
|
|
|
if [[ -n "$php_version" ]]; then |
|
run_cmd rm -f "$(pool_conf_for "$domain" "$php_version")" \ |
|
"$(sock_path_for "$domain" "$php_version")" |
|
run_cmd systemctl restart "php$php_version-fpm" || warn "Khởi động lại php$php_version-fpm thất bại." |
|
ok "Đã xoá pool PHP-FPM" |
|
fi |
|
|
|
if [[ -d "$web_root" ]]; then |
|
read -rp "XOÁ luôn toàn bộ dữ liệu trong $web_root? [y/N] " answer |
|
if [[ "$answer" =~ ^[Yy]$ ]]; then |
|
run_cmd rm -rf "$web_root" |
|
ok "Đã xoá $web_root" |
|
else |
|
warn "Đã giữ lại $web_root" |
|
fi |
|
fi |
|
|
|
if id "$site_user" &>/dev/null; then |
|
read -rp "Xoá luôn user hệ thống $site_user? [y/N] " answer |
|
if [[ "$answer" =~ ^[Yy]$ ]]; then |
|
run_cmd userdel -r "$site_user" 2>/dev/null || run_cmd userdel "$site_user" |
|
ok "Đã xoá user $site_user" |
|
else |
|
warn "Đã giữ lại user $site_user" |
|
fi |
|
fi |
|
|
|
# Database cùng tên slug (nếu có và MySQL/MariaDB đang chạy) |
|
local db_name="$site_user" |
|
if db_service_running && db_exists "$db_name"; then |
|
read -rp "Xoá luôn database '$db_name' và user CSDL cùng tên? (MẤT TOÀN BỘ DỮ LIỆU) [y/N] " answer |
|
if [[ "$answer" =~ ^[Yy]$ ]]; then |
|
run_cmd mysql_client -e "DROP DATABASE \`$db_name\`; DROP USER IF EXISTS '$db_name'@'localhost'; FLUSH PRIVILEGES;" |
|
ok "Đã xoá database và user CSDL '$db_name'" |
|
else |
|
warn "Đã giữ lại database '$db_name'" |
|
fi |
|
fi |
|
|
|
ok "Đã xoá website $domain." |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# list - liệt kê các website |
|
#------------------------------------------------------------------------------- |
|
cmd_list() { |
|
printf "%-30s %-6s %-12s %s\n" "TÊN MIỀN" "PHP" "USER" "KÍCH HOẠT" |
|
printf "%-30s %-6s %-12s %s\n" "--------" "---" "----" "---------" |
|
local v f domain found=0 |
|
for v in "${SUPPORTED_PHP[@]}"; do |
|
for f in /etc/php/"$v"/fpm/pool.d/*.conf; do |
|
[[ -f "$f" ]] || continue |
|
grep -q "Managed by lara" "$f" || continue |
|
domain=$(basename "$f" .conf) |
|
local enabled="chưa" |
|
[[ -L "$NGINX_ENABLED/$domain.conf" ]] && enabled="rồi" |
|
printf "%-30s %-6s %-12s %s\n" "$domain" "$v" "$(site_user_for "$domain")" "$enabled" |
|
found=1 |
|
done |
|
done |
|
((found)) || echo "(chưa có website nào do lara quản lý)" |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# cert - quản lý chứng chỉ SSL qua certbot |
|
#------------------------------------------------------------------------------- |
|
require_certbot() { |
|
command -v certbot >/dev/null 2>&1 && return 0 |
|
# Khi chạy thử (dry-run) chỉ cảnh báo để vẫn xem trước được lệnh certbot. |
|
[[ -n "$DRY_RUN" ]] && { warn "certbot chưa được cài (bỏ qua khi chạy thử)."; return 0; } |
|
die "certbot chưa được cài. Cài bằng: apt install certbot python3-certbot-nginx" |
|
} |
|
|
|
# certbot certificates [--cert-name domain] (chỉ đọc -> chạy trực tiếp) |
|
cmd_cert_status() { |
|
local domain="${1:-}" |
|
require_certbot |
|
if [[ -n "$domain" ]]; then |
|
validate_domain "$domain" |
|
certbot certificates --cert-name "$domain" |
|
else |
|
certbot certificates |
|
fi |
|
} |
|
|
|
cmd_cert_issue() { |
|
local domain="" no_www="" |
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
--no-www) no_www=1; shift ;; |
|
-*) die "Tham số không hợp lệ: $1" ;; |
|
*) if [[ -z "$domain" ]]; then domain="$1"; else die "Tham số thừa: $1"; fi; shift ;; |
|
esac |
|
done |
|
[[ -n "$domain" ]] || die "Cách dùng: $0 cert issue <domain> [--no-www]" |
|
validate_domain "$domain" |
|
require_certbot |
|
|
|
local args=(--nginx -d "$domain") |
|
[[ -z "$no_www" ]] && args+=(-d "www.$domain") |
|
info "Đang phát hành chứng chỉ SSL cho $domain (certbot sẽ tự cập nhật nginx)..." |
|
run_cmd certbot "${args[@]}" |
|
ok "Hoàn tất. certbot đã cài chứng chỉ và tự lên lịch gia hạn (systemd timer)." |
|
} |
|
|
|
cmd_cert_renew() { |
|
local domain="" force="" |
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
--force) force=1; shift ;; |
|
-*) die "Tham số không hợp lệ: $1" ;; |
|
*) if [[ -z "$domain" ]]; then domain="$1"; else die "Tham số thừa: $1"; fi; shift ;; |
|
esac |
|
done |
|
require_certbot |
|
|
|
local args=(renew) |
|
[[ -n "$domain" ]] && { validate_domain "$domain"; args+=(--cert-name "$domain"); } |
|
[[ -n "$force" ]] && args+=(--force-renewal) |
|
|
|
if [[ -n "$domain" ]]; then |
|
info "Đang gia hạn chứng chỉ cho $domain..." |
|
else |
|
info "Đang gia hạn tất cả chứng chỉ sắp hết hạn..." |
|
fi |
|
run_cmd certbot "${args[@]}" |
|
ok "Hoàn tất gia hạn." |
|
} |
|
|
|
cmd_cert_revoke() { |
|
local domain="${1:-}" |
|
[[ -n "$domain" ]] || die "Cách dùng: $0 cert revoke <domain>" |
|
validate_domain "$domain" |
|
require_certbot |
|
|
|
echo "Thao tác này sẽ THU HỒI và XOÁ chứng chỉ của '$domain'." |
|
echo "Website sẽ không còn HTTPS hợp lệ cho tới khi bạn phát hành lại." |
|
read -rp "Tiếp tục? [y/N] " answer |
|
[[ "$answer" =~ ^[Yy]$ ]] || die "Đã huỷ." |
|
|
|
run_cmd certbot revoke --cert-name "$domain" --delete-after-revoke --non-interactive |
|
ok "Đã thu hồi và xoá chứng chỉ của $domain." |
|
} |
|
|
|
cmd_cert() { |
|
local sub="${1:-list}"; shift || true |
|
case "$sub" in |
|
list|"") require_certbot; certbot certificates ;; |
|
status) cmd_cert_status "$@" ;; |
|
issue|generate) cmd_cert_issue "$@" ;; |
|
renew) cmd_cert_renew "$@" ;; |
|
revoke) cmd_cert_revoke "$@" ;; |
|
*) die "Lệnh cert không hợp lệ: $sub (hỗ trợ: list, status, issue, renew, revoke)" ;; |
|
esac |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# version / changelog |
|
#------------------------------------------------------------------------------- |
|
cmd_version() { |
|
echo "lara phiên bản $LARA_VERSION" |
|
} |
|
|
|
cmd_changelog() { |
|
echo -e "${C_GRN}=== LARA - Lịch sử thay đổi (phiên bản hiện tại: $LARA_VERSION) ===${C_RST}" |
|
cat <<'EOF' |
|
|
|
v1.13.0 (2026-06-10) |
|
- 'lara install' dựng thêm banner chào mừng SSH (/etc/update-motd.d/99-lara): |
|
mỗi lần đăng nhập sẽ giới thiệu lệnh lara, hiện phiên bản + số website đang |
|
quản lý. 'lara uninstall' gỡ banner này. |
|
|
|
v1.12.0 (2026-06-10) |
|
- Thêm 'lara fixperms <domain>' + mục menu: áp lại đúng bộ quyền cho một |
|
website. Dùng khi upload code qua SFTP bằng root làm sai chủ sở hữu/quyền |
|
(PHP-FPM mất quyền ghi storage/, nginx mất quyền đọc -> app lỗi). |
|
- Gom logic phân quyền thành hàm dùng chung để 'create' và 'fixperms' luôn |
|
đặt quyền giống hệt nhau. |
|
|
|
v1.11.1 (2026-06-10) |
|
- Cấu hình URL gist công khai cho 'lara update' (LARA_UPDATE_URL_DEFAULT) -> |
|
'sudo lara update' chạy được ngay, không cần đặt biến môi trường. |
|
|
|
v1.11.0 (2026-06-10) |
|
- Thêm 'lara update': tự tải bản mới nhất từ gist công khai (chỉ chứa lara.sh) |
|
qua HTTPS, KIỂM TRA cú pháp + đúng là lara rồi mới cài đè (không 'curl|bash'). |
|
Cấu hình URL gist qua LARA_UPDATE_URL_DEFAULT hoặc biến môi trường |
|
LARA_UPDATE_URL. Có mục tương ứng trong menu. |
|
|
|
v1.10.1 (2026-06-10) |
|
- SỬA LỖI QUAN TRỌNG: 'check' báo nhầm extension bị thiếu một cách NGẪU NHIÊN |
|
(đổi danh sách mỗi lần chạy) dù extension đã nạp. Nguyên nhân: 'php -m | |
|
grep -q' + 'set -o pipefail' - grep -q thoát sớm làm php nhận SIGPIPE khiến |
|
pipeline trả non-zero. Nay lấy 'php -m' một lần vào biến rồi đối chiếu bằng |
|
here-string, hết báo nhầm. (Sửa cùng lỗi cho 'certbot plugins | grep -q'.) |
|
- Sửa hướng dẫn cài extension: ctype/fileinfo/tokenizer thuộc php-common (không |
|
có gói riêng), pdo_mysql thuộc php-mysql; quy đổi đúng tên gói và bỏ trùng. |
|
- 'check' báo rõ khi thiếu trình CLI php<ver> (apt install php<ver>-cli) thay |
|
vì báo nhầm là thiếu hàng loạt extension. |
|
|
|
v1.10.0 (2026-06-10) |
|
- Tự hoàn tác khi 'create' lỗi giữa chừng: dọn sạch user/pool/nginx/thư mục/ |
|
database vừa tạo, không để lại trạng thái dở dang. |
|
- 'deploy' có lưới an toàn: dù lỗi ở bước nào, site luôn được đưa ra khỏi chế |
|
độ bảo trì khi thoát (không còn kẹt trang 503). |
|
- Khoá theo domain (flock) cho create/deploy/remove -> chống chạy song song. |
|
- Mỗi site có thư mục tạm riêng (storage/tmp); bỏ /tmp dùng chung khỏi |
|
open_basedir -> các site không đọc được file tạm/upload của nhau. |
|
- Báo lỗi rõ ràng khi không kết nối được MySQL/MariaDB bằng quyền quản trị. |
|
|
|
v1.9.3 (2026-06-10) |
|
- Chống tạo website trùng triệt để: trước khi tạo, kiểm tra TẤT CẢ tài nguyên |
|
(thư mục mã nguồn, cấu hình + symlink nginx, pool & socket của mọi phiên bản |
|
PHP, user hệ thống, và database nếu dùng --db). Chỉ cần một thành phần đã |
|
tồn tại là dừng ngay, liệt kê rõ và không tạo gì cả. |
|
|
|
v1.9.2 (2026-06-10) |
|
- Sửa lỗi tiềm ẩn với 'set -e': đổi ((errors++)) thành phép gán an toàn để |
|
bộ đếm lỗi trong 'check' không vô tình làm thoát script trên một số bản bash. |
|
|
|
v1.9.1 (2026-06-10) |
|
- 'lara check' kiểm tra certbot kỹ hơn: hiển thị phiên bản và xác minh có |
|
plugin nginx (thứ mà 'lara cert' cần để phát hành/gia hạn chứng chỉ). |
|
|
|
v1.9.0 (2026-06-10) |
|
- Thêm quản lý chứng chỉ SSL qua certbot (lệnh 'lara cert' + menu): |
|
list/status (xem), issue (phát hành, có tuỳ chọn --no-www), |
|
renew (gia hạn tất cả hoặc một site, có --force), revoke (thu hồi & xoá). |
|
|
|
v1.8.0 (2026-06-10) |
|
- Rà soát bảo mật theo OWASP ASVS 5.0: |
|
- Kiểm tra URL git (--repo): chặn transport ext::/fd:: (RCE), file:// |
|
và chuỗi bắt đầu bằng '-' (argument injection). |
|
- Siết quyền file .env về 600 (chỉ chủ sở hữu đọc được). |
|
- Không in mật khẩu CSDL ra màn hình khi đã ghi vào .env. |
|
- Hàm log dùng printf '%s' để tránh chèn mã ANSI/giả mạo dòng log. |
|
|
|
v1.7.0 (2026-06-10) |
|
- Thêm phiên bản cho script (lệnh 'lara version'). |
|
- Thêm changelog nhúng trong script (lệnh 'lara changelog' + mục menu). |
|
|
|
v1.6.0 (2026-06-10) |
|
- Thêm chế độ chạy thử --dry-run cho mọi lệnh: các bước kiểm tra vẫn chạy |
|
thật, mọi thay đổi hệ thống chỉ được in ra (kèm nội dung file sẽ ghi). |
|
- Dry-run không yêu cầu quyền root; menu có nút bật/tắt (mục 8). |
|
|
|
v1.5.0 (2026-06-10) |
|
- 'lara check' kiểm tra thêm MySQL/MariaDB và Redis (cảnh báo, không chặn). |
|
- Tuỳ chọn --db: tạo database + user CSDL riêng cho website (mật khẩu |
|
ngẫu nhiên, utf8mb4, GRANT chỉ trên database của chính site đó); |
|
tự ghi thông tin vào .env nếu có. |
|
- 'lara remove' hỏi xoá database cùng tên slug (mặc định giữ lại). |
|
|
|
v1.4.0 (2026-06-10) |
|
- Nguồn mã khi tạo website: chọn giữa "chỉ tạo thư mục trống" (kèm |
|
public/index.php tạm) hoặc "clone từ git" (bỏ composer create-project). |
|
|
|
v1.3.0 (2026-06-10) |
|
- Menu tương tác: chạy 'lara' không tham số để mở; điều hướng bằng phím |
|
mũi tên hoặc gõ số + Enter; menu đa cấp, nhập 0 để quay lại/thoát. |
|
|
|
v1.2.0 (2026-06-10) |
|
- Chuyển toàn bộ giao diện sang tiếng Việt. |
|
|
|
v1.1.0 (2026-06-10) |
|
- Đổi tên script thành lara.sh; thêm lệnh install/uninstall để tự cài |
|
vào /usr/local/bin/lara. |
|
- Mở rộng hỗ trợ Ubuntu 18.04 - 26.04 LTS. |
|
|
|
v1.0.0 (2026-06-10) |
|
- Bản đầu tiên: create / deploy / remove / list / check. |
|
- PHP 7.4 hoặc 8.4, Laravel 8-13 (ràng buộc tương thích tự động). |
|
- Mỗi website một user hệ thống riêng, pool PHP-FPM + socket riêng, |
|
vhost nginx chỉ HTTP, in sẵn lệnh certbot để người dùng tự chạy. |
|
EOF |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# install / uninstall - tự cài script vào PATH |
|
#------------------------------------------------------------------------------- |
|
# Dựng banner MOTD giới thiệu lara, hiển thị mỗi khi đăng nhập SSH. |
|
# Banner tự đọc phiên bản + đếm số website tại thời điểm đăng nhập. |
|
install_welcome_banner() { |
|
if [[ ! -d /etc/update-motd.d ]]; then |
|
warn "Không có /etc/update-motd.d - bỏ qua banner chào mừng SSH." |
|
return 0 |
|
fi |
|
write_file "$MOTD_PATH" <<'BANNER' |
|
#!/bin/bash |
|
# Banner giới thiệu lara - tạo bởi 'lara install', gỡ bởi 'lara uninstall'. |
|
command -v lara >/dev/null 2>&1 || exit 0 |
|
C_C='\033[1;36m'; C_Y='\033[1;33m'; C_D='\033[0;90m'; C_R='\033[0m' |
|
ver=$(lara version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') |
|
sites=$(grep -lr 'Managed by lara' /etc/php/*/fpm/pool.d/ 2>/dev/null | wc -l | tr -d ' ') |
|
printf '\n' |
|
printf " ${C_C}lara${C_R} ${C_D}v%s${C_R} — Công cụ quản lý website PHP/Laravel\n" "${ver:-?}" |
|
printf " ${C_D}%s website đang được lara quản lý${C_R}\n\n" "${sites:-0}" |
|
printf " ${C_Y}sudo lara${C_R} mở menu quản lý (tạo / deploy / SSL / sửa quyền...)\n" |
|
printf " ${C_Y}lara list${C_R} xem danh sách website\n" |
|
printf " ${C_Y}sudo lara update${C_R} cập nhật lara lên bản mới nhất\n\n" |
|
BANNER |
|
run_cmd chmod 0755 "$MOTD_PATH" |
|
ok "Đã cài banner chào mừng SSH ($MOTD_PATH)" |
|
} |
|
|
|
cmd_install() { |
|
local src |
|
src=$(readlink -f "$0" 2>/dev/null) || src="$0" |
|
[[ -f "$src" ]] || die "Không xác định được vị trí file script ($0) - hãy chạy install trực tiếp từ file script." |
|
if [[ "$src" == "$INSTALL_PATH" ]]; then |
|
ok "lara đã được cài sẵn tại $INSTALL_PATH" |
|
else |
|
run_cmd install -m 0755 "$src" "$INSTALL_PATH" |
|
ok "Đã cài vào $INSTALL_PATH" |
|
fi |
|
install_welcome_banner # luôn (cài lại) banner chào mừng SSH |
|
info "Giờ bạn có thể chạy lara từ bất cứ đâu, ví dụ: sudo lara create example.com --php 8.4 --laravel 12" |
|
} |
|
|
|
cmd_uninstall() { |
|
local removed="" |
|
if [[ -f "$INSTALL_PATH" ]]; then |
|
run_cmd rm -f "$INSTALL_PATH" |
|
ok "Đã gỡ $INSTALL_PATH (các website, pool FPM và cấu hình nginx hiện có vẫn giữ nguyên)" |
|
removed=1 |
|
fi |
|
if [[ -f "$MOTD_PATH" ]]; then |
|
run_cmd rm -f "$MOTD_PATH" |
|
ok "Đã gỡ banner chào mừng SSH ($MOTD_PATH)" |
|
removed=1 |
|
fi |
|
[[ -n "$removed" ]] || warn "Không có gì để gỡ - $INSTALL_PATH không tồn tại." |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# update - tự tải bản lara mới nhất từ gist công khai rồi cài đè. |
|
# An toàn: tải về file tạm -> KIỂM TRA (cú pháp + đúng là lara) -> mới cài. |
|
# Không dùng kiểu 'curl | bash'. |
|
#------------------------------------------------------------------------------- |
|
cmd_update() { |
|
local url="${LARA_UPDATE_URL:-$LARA_UPDATE_URL_DEFAULT}" |
|
case "$url" in |
|
*REPLACE_WITH_GIST_ID*|"") |
|
die "Chưa cấu hình nguồn cập nhật. |
|
Hãy tạo gist công khai chứa lara.sh, rồi đặt URL raw 'mới nhất' của nó vào |
|
LARA_UPDATE_URL_DEFAULT trong script, hoặc chạy: |
|
sudo LARA_UPDATE_URL=\"https://gist.githubusercontent.com/<user>/<id>/raw/lara.sh\" lara update" ;; |
|
https://*) : ;; |
|
*) die "URL cập nhật phải dùng HTTPS: $url" ;; |
|
esac |
|
command -v curl >/dev/null 2>&1 || die "Cần 'curl' để cập nhật: apt install curl" |
|
|
|
info "Đang tải bản mới nhất từ: $url" |
|
local tmp; tmp=$(mktemp) || die "Không tạo được file tạm." |
|
curl --proto '=https' --tlsv1.2 -fsSL "$url" -o "$tmp" \ |
|
|| { rm -f "$tmp"; die "Tải thất bại - kiểm tra URL hoặc kết nối mạng."; } |
|
|
|
# Kiểm tra trước khi cài: cú pháp hợp lệ và đúng là file lara. |
|
bash -n "$tmp" 2>/dev/null || { rm -f "$tmp"; die "File tải về lỗi cú pháp - KHÔNG cài (có thể tải dở/hỏng)."; } |
|
grep -q '^LARA_VERSION=' "$tmp" || { rm -f "$tmp"; die "File tải về không giống lara.sh - KHÔNG cài."; } |
|
|
|
local new_ver; new_ver=$(grep -m1 '^LARA_VERSION=' "$tmp" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+') |
|
info "Bản đang chạy: $LARA_VERSION -> bản tải về: ${new_ver:-?}" |
|
if [[ "$new_ver" == "$LARA_VERSION" ]]; then |
|
warn "Đã là bản mới nhất ($LARA_VERSION); vẫn cài lại cho chắc." |
|
fi |
|
|
|
run_cmd install -m 0755 "$tmp" "$INSTALL_PATH" |
|
rm -f "$tmp" |
|
ok "Đã cập nhật lara: $LARA_VERSION -> ${new_ver:-?} ($INSTALL_PATH)" |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# Menu tương tác |
|
#------------------------------------------------------------------------------- |
|
pause() { |
|
echo |
|
read -rp " Nhấn Enter để quay lại menu..." _ |
|
} |
|
|
|
clear_screen() { printf '\033[2J\033[H'; } |
|
|
|
# Đọc một phím bấm, kết quả đặt vào biến toàn cục KEY: |
|
# up / down / enter / backspace / digit:<n> / other |
|
read_key() { |
|
local k rest="" |
|
IFS= read -rsn1 k || { KEY=other; return; } |
|
case "$k" in |
|
$'\x1b') |
|
if IFS= read -rsn2 -t 1 rest 2>/dev/null; then |
|
case "$rest" in |
|
'[A') KEY=up ;; |
|
'[B') KEY=down ;; |
|
*) KEY=other ;; |
|
esac |
|
else |
|
KEY=other |
|
fi |
|
;; |
|
"") KEY=enter ;; |
|
$'\x7f') KEY=backspace ;; |
|
[0-9]) KEY="digit:$k" ;; |
|
*) KEY=other ;; |
|
esac |
|
} |
|
|
|
# menu_select "Tiêu đề" "Nhãn mục 0" "mục 1" "mục 2" ... |
|
# Điều khiển: phím ↑/↓ + Enter, hoặc gõ số + Enter. Mục 0 = quay lại/thoát. |
|
# Kết quả đặt vào biến toàn cục MENU_CHOICE (0..N). |
|
MENU_CHOICE=0 |
|
menu_select() { |
|
local title="$1" back_label="$2"; shift 2 |
|
local items=("$@") n=$# sel=1 buf="" i choice |
|
while true; do |
|
clear_screen |
|
echo -e "${C_GRN}=== $title ===${C_RST}" |
|
echo |
|
for ((i = 1; i <= n; i++)); do |
|
if [[ -z "$buf" && $sel -eq $i ]]; then |
|
echo -e " ${C_GRN}> $i. ${items[i-1]}${C_RST}" |
|
else |
|
echo " $i. ${items[i-1]}" |
|
fi |
|
done |
|
if [[ -z "$buf" && $sel -eq 0 ]]; then |
|
echo -e " ${C_GRN}> 0. $back_label${C_RST}" |
|
else |
|
echo " 0. $back_label" |
|
fi |
|
echo |
|
echo " Hướng dẫn: dùng phím ↑/↓ rồi Enter, hoặc gõ số rồi Enter." |
|
echo " Nhập 0 để: $back_label." |
|
printf ' Lựa chọn: %s' "$buf" |
|
|
|
read_key |
|
case "$KEY" in |
|
up) buf=""; sel=$((sel - 1)); (( sel < 0 )) && sel=$n ;; |
|
down) buf=""; sel=$((sel + 1)); (( sel > n )) && sel=0 ;; |
|
digit:*) buf+="${KEY#digit:}" ;; |
|
backspace) buf="${buf%?}" ;; |
|
enter) |
|
if [[ -n "$buf" ]]; then |
|
choice=$((10#$buf)) |
|
else |
|
choice=$sel |
|
fi |
|
if (( choice >= 0 && choice <= n )); then |
|
MENU_CHOICE=$choice |
|
echo |
|
return 0 |
|
fi |
|
buf="" # số ngoài phạm vi -> nhập lại |
|
;; |
|
esac |
|
done |
|
} |
|
|
|
# ask_text "Câu hỏi" -> kết quả vào REPLY_TEXT; trả về 1 nếu người dùng nhập 0 |
|
ask_text() { |
|
echo |
|
read -rp " $1 (nhập 0 để quay lại): " REPLY_TEXT |
|
[[ "$REPLY_TEXT" != "0" ]] |
|
} |
|
|
|
# Liệt kê các domain do lara quản lý (mỗi dòng một domain) |
|
managed_sites() { |
|
local v f |
|
for v in "${SUPPORTED_PHP[@]}"; do |
|
for f in /etc/php/"$v"/fpm/pool.d/*.conf; do |
|
[[ -f "$f" ]] || continue |
|
grep -q "Managed by lara" "$f" || continue |
|
basename "$f" .conf |
|
done |
|
done |
|
} |
|
|
|
# Chạy chính script này như một tiến trình con để lỗi (die/set -e) |
|
# không làm thoát menu, sau đó dừng cho người dùng đọc kết quả. |
|
run_cli() { |
|
clear_screen |
|
if [[ -n "$DRY_RUN" ]]; then |
|
"$BASH" "$0" --dry-run "$@" || true |
|
else |
|
"$BASH" "$0" "$@" || true |
|
fi |
|
pause |
|
} |
|
|
|
menu_create() { |
|
local laravel_version php_version repo_url="" |
|
|
|
menu_select "Tạo website mới - chọn phiên bản Laravel" "Quay lại menu chính" \ |
|
"Laravel 8 (PHP 7.4)" \ |
|
"Laravel 11 (PHP 8.4)" \ |
|
"Laravel 12 (PHP 8.4)" \ |
|
"Laravel 13 (PHP 8.4)" |
|
case $MENU_CHOICE in |
|
0) return ;; |
|
1) laravel_version=8; php_version=7.4 ;; |
|
2) laravel_version=11; php_version=8.4 ;; |
|
3) laravel_version=12; php_version=8.4 ;; |
|
4) laravel_version=13; php_version=8.4 ;; |
|
esac |
|
|
|
menu_select "Tạo website mới - chọn nguồn mã" "Quay lại menu chính" \ |
|
"Chỉ tạo thư mục trống (tự đưa mã nguồn lên sau)" \ |
|
"Clone từ git repository" |
|
case $MENU_CHOICE in |
|
0) return ;; |
|
2) ask_text "Nhập URL git repository" || return |
|
repo_url="$REPLY_TEXT" ;; |
|
esac |
|
|
|
menu_select "Tạo database MySQL/MariaDB riêng cho website?" "Quay lại menu chính" \ |
|
"Có - tạo database + user CSDL riêng (mật khẩu ngẫu nhiên)" \ |
|
"Không - tự cấu hình cơ sở dữ liệu sau" |
|
local want_db="" |
|
case $MENU_CHOICE in |
|
0) return ;; |
|
1) want_db=1 ;; |
|
esac |
|
|
|
ask_text "Nhập tên miền (ví dụ: example.com)" || return |
|
local domain="$REPLY_TEXT" |
|
|
|
local args=(create "$domain" --php "$php_version" --laravel "$laravel_version") |
|
[[ -n "$repo_url" ]] && args+=(--repo "$repo_url") |
|
[[ -n "$want_db" ]] && args+=(--db) |
|
run_cli "${args[@]}" |
|
} |
|
|
|
# Chọn một website do lara quản lý -> đặt vào PICKED_SITE. |
|
# Trả về 1 (và để PICKED_SITE rỗng) nếu không có site nào hoặc người dùng quay lại. |
|
PICKED_SITE="" |
|
pick_managed_site() { |
|
local title="$1" |
|
PICKED_SITE="" |
|
local sites=() d |
|
while IFS= read -r d; do sites+=("$d"); done < <(managed_sites) |
|
|
|
if (( ${#sites[@]} == 0 )); then |
|
clear_screen |
|
warn "Chưa có website nào do lara quản lý trên máy chủ này." |
|
pause |
|
return 1 |
|
fi |
|
|
|
menu_select "$title" "Quay lại menu chính" "${sites[@]}" |
|
(( MENU_CHOICE == 0 )) && return 1 |
|
PICKED_SITE="${sites[MENU_CHOICE-1]}" |
|
return 0 |
|
} |
|
|
|
# menu_pick_site <deploy|remove> "Tiêu đề" |
|
menu_pick_site() { |
|
local action="$1" title="$2" |
|
pick_managed_site "$title" || return |
|
run_cli "$action" "$PICKED_SITE" |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# Menu chứng chỉ SSL |
|
#------------------------------------------------------------------------------- |
|
menu_cert_issue() { |
|
pick_managed_site "Phát hành chứng chỉ - chọn website" || return |
|
local domain="$PICKED_SITE" |
|
menu_select "Bao gồm cả www.$domain trong chứng chỉ?" "Quay lại menu chính" \ |
|
"Có - $domain và www.$domain (www phải đã trỏ DNS)" \ |
|
"Không - chỉ $domain" |
|
case $MENU_CHOICE in |
|
0) return ;; |
|
1) run_cli cert issue "$domain" ;; |
|
2) run_cli cert issue "$domain" --no-www ;; |
|
esac |
|
} |
|
|
|
menu_cert_renew() { |
|
menu_select "Gia hạn chứng chỉ" "Quay lại menu chính" \ |
|
"Gia hạn TẤT CẢ chứng chỉ sắp hết hạn" \ |
|
"Gia hạn cho một website cụ thể" |
|
case $MENU_CHOICE in |
|
0) return ;; |
|
1) run_cli cert renew ;; |
|
2) pick_managed_site "Gia hạn chứng chỉ - chọn website" || return |
|
run_cli cert renew "$PICKED_SITE" ;; |
|
esac |
|
} |
|
|
|
menu_cert_revoke() { |
|
pick_managed_site "Thu hồi & xoá chứng chỉ - chọn website" || return |
|
run_cli cert revoke "$PICKED_SITE" |
|
} |
|
|
|
menu_cert() { |
|
while true; do |
|
menu_select "Quản lý chứng chỉ SSL (certbot)" "Quay lại menu chính" \ |
|
"Xem danh sách / trạng thái chứng chỉ" \ |
|
"Phát hành chứng chỉ mới cho website" \ |
|
"Gia hạn chứng chỉ" \ |
|
"Thu hồi & xoá chứng chỉ" |
|
case $MENU_CHOICE in |
|
0) return ;; |
|
1) run_cli cert list ;; |
|
2) menu_cert_issue ;; |
|
3) menu_cert_renew ;; |
|
4) menu_cert_revoke ;; |
|
esac |
|
done |
|
} |
|
|
|
menu_check() { |
|
menu_select "Kiểm tra môi trường máy chủ" "Quay lại menu chính" \ |
|
"Kiểm tra tất cả các phiên bản PHP" \ |
|
"Chỉ kiểm tra PHP 7.4" \ |
|
"Chỉ kiểm tra PHP 8.4" |
|
case $MENU_CHOICE in |
|
0) return ;; |
|
1) run_cli check ;; |
|
2) run_cli check --php 7.4 ;; |
|
3) run_cli check --php 8.4 ;; |
|
esac |
|
} |
|
|
|
menu_main() { |
|
local dry_label title |
|
while true; do |
|
if [[ -n "$DRY_RUN" ]]; then |
|
dry_label="Chế độ chạy thử (dry-run): ĐANG BẬT - chọn để tắt" |
|
title="LARA v$LARA_VERSION - Quản lý website PHP/Laravel [DRY-RUN]" |
|
else |
|
dry_label="Chế độ chạy thử (dry-run): đang tắt - chọn để bật" |
|
title="LARA v$LARA_VERSION - Quản lý website PHP/Laravel" |
|
fi |
|
menu_select "$title" "Thoát" \ |
|
"Tạo website mới" \ |
|
"Deploy website (cập nhật mã nguồn)" \ |
|
"Sửa quyền website (sau khi upload code/SFTP)" \ |
|
"Xoá website" \ |
|
"Quản lý chứng chỉ SSL (certbot)" \ |
|
"Danh sách website" \ |
|
"Kiểm tra môi trường máy chủ" \ |
|
"Cài lara vào $INSTALL_PATH" \ |
|
"Cập nhật lara lên bản mới nhất" \ |
|
"Gỡ lara khỏi $INSTALL_PATH" \ |
|
"$dry_label" \ |
|
"Lịch sử thay đổi (changelog)" |
|
case $MENU_CHOICE in |
|
0) clear_screen; echo "Tạm biệt!"; exit 0 ;; |
|
1) menu_create ;; |
|
2) menu_pick_site deploy "Deploy website - chọn tên miền" ;; |
|
3) menu_pick_site fixperms "Sửa quyền website - chọn tên miền" ;; |
|
4) menu_pick_site remove "Xoá website - chọn tên miền" ;; |
|
5) menu_cert ;; |
|
6) clear_screen; cmd_list; pause ;; |
|
7) menu_check ;; |
|
8) run_cli install ;; |
|
9) run_cli update ;; |
|
10) run_cli uninstall ;; |
|
11) if [[ -n "$DRY_RUN" ]]; then DRY_RUN=""; else DRY_RUN=1; fi ;; |
|
12) clear_screen; cmd_changelog; pause ;; |
|
esac |
|
done |
|
} |
|
|
|
#------------------------------------------------------------------------------- |
|
# main |
|
#------------------------------------------------------------------------------- |
|
# Ở chế độ dry-run không bắt buộc quyền root (không có gì được thay đổi thật) |
|
maybe_require_root() { |
|
[[ -n "$DRY_RUN" ]] || require_root |
|
} |
|
|
|
main() { |
|
# Tách cờ toàn cục --dry-run ra khỏi danh sách tham số |
|
local args=() a |
|
for a in "$@"; do |
|
if [[ "$a" == "--dry-run" ]]; then DRY_RUN=1; else args+=("$a"); fi |
|
done |
|
set -- "${args[@]+"${args[@]}"}" |
|
|
|
local cmd="${1:-}" |
|
if [[ -z "$cmd" ]]; then |
|
if [[ -t 0 && -t 1 ]]; then |
|
menu_main |
|
else |
|
usage 1 # không có terminal tương tác -> chỉ in hướng dẫn |
|
fi |
|
fi |
|
shift || true |
|
|
|
if [[ -n "$DRY_RUN" && "$cmd" != "menu" ]]; then |
|
warn "Chế độ DRY-RUN: chỉ hiển thị các thay đổi, KHÔNG thực thi gì cả." |
|
fi |
|
|
|
case "$cmd" in |
|
menu) menu_main ;; |
|
install) maybe_require_root; cmd_install ;; |
|
update) maybe_require_root; cmd_update ;; |
|
uninstall) maybe_require_root; cmd_uninstall ;; |
|
create) maybe_require_root; cmd_create "$@" ;; |
|
deploy) maybe_require_root; cmd_deploy "$@" ;; |
|
remove) maybe_require_root; cmd_remove "$@" ;; |
|
fixperms) maybe_require_root; cmd_fixperms "$@" ;; |
|
cert) maybe_require_root; cmd_cert "$@" ;; |
|
list) cmd_list ;; |
|
check) |
|
local php="" |
|
[[ "${1:-}" == "--php" ]] && php="${2:-}" |
|
[[ -n "$php" ]] && validate_php_version "$php" |
|
check_environment "$php" |
|
;; |
|
version|--version|-v) cmd_version ;; |
|
changelog) cmd_changelog ;; |
|
-h|--help|help) usage ;; |
|
*) die "Lệnh không hợp lệ: $cmd (các lệnh hỗ trợ: install, update, create, deploy, remove, fixperms, cert, list, check, version, changelog, uninstall)" ;; |
|
esac |
|
} |
|
|
|
main "$@" |