2026-10-26 13:38:45 +00:00
#!/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 <artifact.tar.gz> --dest <appDir/releases/<ver>> 릴리스 디렉터리를 비우고 artifact 를 푼다
# write --file <appDir 아래 파일> 표준 입력을 파일로 쓴다(REVISION.json·.previous.json)
# switch --link <appDir/current> --target <릴리스 디렉터리> 심링크 원자 교체(ln → mv -T)
2026-10-26 13:56:42 +00:00
# rename --dir <releases 아래 디렉터리> --target <releases 아래 새 이름> 디렉터리 이름 바꾸기(같은 버전 재설치 때 실행 중인 디렉터리 보존)
2026-10-26 13:38:45 +00:00
# remove --dir <appDir/releases/<old>> 오래된 릴리스 삭제(releases 아래만)
# backup-db --db <sqlite 파일> --out <백업 파일> [--node <node>] SQLite 온라인 백업(VACUUM INTO)
# migrate --cwd <릴리스 디렉터리> --script <상대 경로> [--env-file <f>] [--node <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 " ;;
2026-10-26 13:56:42 +00:00
rename)
inside " $DIR " ; inside " $TARGET " ; case " $DIR " in " $APP_DIR " /releases/*) ;; *) echo "--dir 는 releases 아래여야 합니다" >& 2; exit 3;; esac ; case " $TARGET " in " $APP_DIR " /releases/*) ;; *) echo "--target 은 releases 아래여야 합니다" >& 2; exit 3;; esac
mv -T " $DIR " " $TARGET " ; echo "renamed $DIR -> $TARGET " ;;
2026-10-26 13:38:45 +00:00
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 를 만드는 문제를 막는다.
2026-10-26 14:03:43 +00:00
# systemd EnvironmentFile 문법(KEY=값, 값에 공백·따옴표 가능)은 bash 소스가 아니다 — 줄 단위로 읽어 따옴표를 벗기고 export 한다(실제 환경: MAIL_FROM_NAME=Ari Hak dang 에서 소스 실패).
if [ -n " $ENV_FILE " ] ; then [ -r " $ENV_FILE " ] || { echo "EnvironmentFile 을 읽을 수 없습니다: $ENV_FILE " >& 2; exit 2; }
while IFS = read -r line || [ -n " $line " ] ; do line = " ${ line % $'\r' } " ; case " $line " in '' | '#' *) continue ;; esac ; key = " ${ line %%=* } " ; val = " ${ line #*= } " ; key = " ${ key #export } " ; key = " ${ key // / } "
case " $key " in *[ !A-Za-z0-9_] *| '' ) continue ;; esac ; case " $val " in \" *\" ) val = " ${ val # \" } " ; val = " ${ val % \" } " ;; \' *\' ) val = " ${ val # \' } " ; val = " ${ val % \' } " ;; esac ; export " $key = $val " ; done < " $ENV_FILE " ; fi
2026-10-26 13:38:45 +00:00
cd " $CWD " ; exec " $NODE_BIN " --no-warnings " $SCRIPT " ;;
*) echo "알 수 없는 하위 명령: $SUB " >& 2; exit 2;;
esac