#!/bin/bash # deploy/module-as-user.sh — 도입 모듈을 "그 모듈의 실행 계정" 으로 설치하기 위한 최소 파일 작업 도우미(§32). # # 왜 — Node Agent 는 master 계정으로 돌지만 도입 모듈(AriMail 등)은 서버에 이미 다른 계정(arihakdang 등)으로 설치되어 있고 # 그 디렉터리(/srv/<모듈>/releases, data)는 그 계정만 쓸 수 있다. Agent 가 직접 쓰면 EACCES 로 끝나므로(실제 환경 검증에서 발견), # sudoers(deploy/sudoers/ariweb-agent)에 "이 스크립트만 root 가 아닌 임의 계정으로" 허용하고 Agent 가 `sudo -n -u <계정> bash <이 파일> <하위 명령>` 으로 부른다. # 하위 명령은 모듈 배치에 필요한 것만 두고, 모든 경로 인자는 --app-dir 아래(또는 명시한 artifact)로 제한한다. 비밀값은 다루지 않고 출력하지 않는다. # # 사용: module-as-user.sh <하위 명령> --app-dir <모듈 디렉터리> [인자…] # extract --tgz --dest > 릴리스 디렉터리를 비우고 artifact 를 푼다 # write --file 표준 입력을 파일로 쓴다(REVISION.json·.previous.json) # switch --link --target <릴리스 디렉터리> 심링크 원자 교체(ln → mv -T) # remove --dir > 오래된 릴리스 삭제(releases 아래만) # backup-db --db --out <백업 파일> [--node ] SQLite 온라인 백업(VACUUM INTO) # migrate --cwd <릴리스 디렉터리> --script <상대 경로> [--env-file ] [--node ] EnvironmentFile 을 읽어 마이그레이션 실행 set -euo pipefail SUB="${1:-}"; shift || true APP_DIR=""; TGZ=""; DEST=""; FILE=""; LINK=""; TARGET=""; DIR=""; DB=""; OUT=""; NODE_BIN=""; CWD=""; SCRIPT=""; ENV_FILE="" while [ $# -gt 0 ]; do case "$1" in --app-dir) APP_DIR="$2"; shift 2;; --tgz) TGZ="$2"; shift 2;; --dest) DEST="$2"; shift 2;; --file) FILE="$2"; shift 2;; --link) LINK="$2"; shift 2;; --target) TARGET="$2"; shift 2;; --dir) DIR="$2"; shift 2;; --db) DB="$2"; shift 2;; --out) OUT="$2"; shift 2;; --node) NODE_BIN="$2"; shift 2;; --cwd) CWD="$2"; shift 2;; --script) SCRIPT="$2"; shift 2;; --env-file) ENV_FILE="$2"; shift 2;; *) echo "알 수 없는 인자 $1" >&2; exit 2;; esac; done [ -n "$APP_DIR" ] && [ -d "$APP_DIR" ] || { echo "--app-dir <존재하는 디렉터리> 가 필요합니다" >&2; exit 2; } APP_DIR="$(cd "$APP_DIR" && pwd -P)" # 경로가 appDir 아래인지(심링크를 따라간 실제 경로 기준, 아직 없는 경로는 부모 기준) 확인한다 — 다른 디렉터리를 건드리지 못하게. inside() { local p="$1" d real; d="$(dirname "$p")"; while [ ! -d "$d" ] && [ "$d" != "/" ]; do d="$(dirname "$d")"; done; real="$(cd "$d" && pwd -P)"; case "$real" in "$APP_DIR"|"$APP_DIR"/*) return 0;; esac; echo "경로가 모듈 디렉터리($APP_DIR) 밖입니다: $p" >&2; exit 3; } [ -n "$NODE_BIN" ] || NODE_BIN="$(command -v node || true)" case "$SUB" in extract) [ -f "$TGZ" ] || { echo "artifact 가 없습니다: $TGZ" >&2; exit 2; }; inside "$DEST"; case "$DEST" in "$APP_DIR"/releases/*) ;; *) echo "--dest 는 releases 아래여야 합니다" >&2; exit 3;; esac rm -rf "$DEST"; mkdir -p "$DEST"; tar -xzf "$TGZ" -C "$DEST"; echo "extracted $DEST";; write) inside "$FILE"; mkdir -p "$(dirname "$FILE")"; tmp="$FILE.tmp-$$"; cat > "$tmp"; mv -f "$tmp" "$FILE"; echo "written $FILE";; switch) inside "$LINK"; inside "$TARGET"; [ -d "$TARGET" ] || { echo "대상 디렉터리가 없습니다: $TARGET" >&2; exit 2; } ln -sfn "$TARGET" "$LINK.tmp-$$"; mv -Tf "$LINK.tmp-$$" "$LINK"; echo "switched $LINK -> $TARGET";; remove) inside "$DIR"; case "$DIR" in "$APP_DIR"/releases/*) ;; *) echo "--dir 는 releases 아래여야 합니다" >&2; exit 3;; esac; rm -rf "$DIR"; echo "removed $DIR";; backup-db) inside "$OUT"; [ -f "$DB" ] || { echo "skipped no_db"; exit 0; }; mkdir -p "$(dirname "$OUT")" # node:sqlite 의 VACUUM INTO 로 WAL 포함 일관된 사본을 만든다(원본 deploy.sh 의 python backup 과 같은 목적). "$NODE_BIN" --no-warnings -e "const {DatabaseSync}=require('node:sqlite');const d=new DatabaseSync(process.argv[1],{readOnly:true});try{d.exec(\"VACUUM INTO '\"+process.argv[2].replace(/'/g,\"''\")+\"'\")}finally{d.close()}" "$DB" "$OUT"; echo "backup $OUT";; migrate) inside "$CWD"; [ -f "$CWD/$SCRIPT" ] || { echo "skipped no_migrate"; exit 0; } # systemd 유닛의 EnvironmentFile(DB_FILE·DATA_DIR 등)을 같은 계정으로 읽어 환경에 싣는다 — 없으면 모듈 기본 경로(릴리스 디렉터리 안)를 쓰게 되어 운영 DB 가 아닌 빈 DB 를 만드는 문제를 막는다. if [ -n "$ENV_FILE" ]; then [ -r "$ENV_FILE" ] || { echo "EnvironmentFile 을 읽을 수 없습니다: $ENV_FILE" >&2; exit 2; }; set -a; . "$ENV_FILE"; set +a; fi cd "$CWD"; exec "$NODE_BIN" --no-warnings "$SCRIPT";; *) echo "알 수 없는 하위 명령: $SUB" >&2; exit 2;; esac