Profile picture

[Shell Script] 팰월드(Palworld) 세이브 데이터 백업 스크립트

JaehyoJJAng2024년 01월 24일

◾️ 관련 링크


◾️ 게임 데이터 백업

팰월드 멀티 서버의 경우 아직 불안정한 단계이기에 사용자들의 데이터가 모종의 이유(?)로 날아가면 난감하기에 주기적으로 백업을 해주어야 한다.

백업하는 과정을 스크립트로 작성하고 마무리로 텔레그램으로 알림까지 보내는 간단한 백업용 알림 스크립트를 작성해보자.


Palworld의 세이브 데이터는 아래와 같은 경로에 존재한다.

ls -lh game/Pal/Saved/SaveGames/0/4D308CFCDF584AFCAB7E66F8BB5A881D/Players/
total 44K
-rw-r--r-- 1 master master 2.7K Jan 26 00:58 04637D7E000000000000000000000000.sav
-rw-r--r-- 1 master master 3.3K Jan 26 14:02 26BF7B8D000000000000000000000000.sav
-rw-r--r-- 1 master master 2.6K Jan 26 00:58 2EA7D45B000000000000000000000000.sav
-rw-r--r-- 1 master master 3.8K Jan 26 14:02 48FFF339000000000000000000000000.sav
-rw-r--r-- 1 master master 2.4K Jan 26 00:58 62E1ACE8000000000000000000000000.sav
-rw-r--r-- 1 master master 2.8K Jan 26 12:58 7FB221FA000000000000000000000000.sav
-rw-r--r-- 1 master master 3.5K Jan 26 00:58 923FD405000000000000000000000000.sav
-rw-r--r-- 1 master master 3.2K Jan 26 14:02 A15D2445000000000000000000000000.sav
-rw-r--r-- 1 master master 2.6K Jan 26 00:58 A4ABAE08000000000000000000000000.sav
-rw-r--r-- 1 master master 2.6K Jan 26 00:58 C01C1509000000000000000000000000.sav
-rw-r--r-- 1 master master 2.5K Jan 26 00:58 C57E1CD9000000000000000000000000.sav

각 플레이어들의 정보가 저장되어 있지만 누구의 세이브 데이터인지 알 수가 없게 되어있다.
유저 데이터를 쉽게 찾고 싶다면 아래 게시글을 참고하도록 하자
유저 데이터 찾기


이제부터 스크립트를 작성해볼 것이다.
위 경로에 존재하는 각 사용자들의 세이브 폴더들을 gzip으로 압축하여 백업하고
백업한 정보를 텔레그램으로 알림을 보내볼 것이다.

그리고 해당 스크립트는 crontab에 등록하여 주기적으로 백업하는 프로세스를 만들어주면 끝이다.


▪️ 백업 스크립트

  • 유저 데이터 백업 스크립트

~/backup/script.sh

#!/usr/bin/bash

HOST='master'
SERVER_NAME="palworld-server"
if $(docker ps | grep $SERVER_NAME >/dev/null 2>&1); then
    {% raw %} if [[ $(docker inspect --format '{{.State.Status }}' "$SERVER_NAME") == 'running' ]]; {% endraw %} then
        echo "$SERVER_NAME is running"
    # 도커 프로세스에 해당 서버가 있는 경우 & 서버가 실행 중이지 않은 경우
    else
        echo "$SERVER_NAME is paused"
        exit 1
    fi
else
    echo "$SERVER_NAME is stoped"
    exit 1
fi

# 압축할 디렉토리
source_dir="/home/${HOST}/palworld/game/Pal/Saved/SaveGames"

# 압축 파일이 저장될 디렉토리
backup_dir="/tmp/palworld/backup"

# 압축 파일 이름
DATE="$(date +'%Y%m%d_%H%M')"
backup_filename="palworld_backup_${DATE}.tar.gz"

# 백업 디렉토리 없는 경우 생성
if [[ ! -d "${backup_dir}" ]]; then
    mkdir -p "${backup_dir}"
fi

# 로그 파일 생성
LOG_FILE_NM="/tmp/backup.log"
touch "${LOG_FILE_NM}"

{
    echo -e "\n💿 PalWorld 백업 시작 ... 💿"

    # 디렉토리를 gzip 압축
    tar -czf "${backup_dir}/${backup_filename}" "${source_dir}" 1>/dev/null 2>&1
    NAME=$(ls -lh "${backup_dir}/${backup_filename}" | awk '{print $9}')
    SIZE=$(ls -lh "${backup_dir}/${backup_filename}" | awk '{print $5}')
    
    echo "==== 백업 파일 정보 ===="
    echo "파일명 : ${NAME}"
    echo "파일 크기 : ${SIZE}"
    echo

    echo "==== 이전 백업파일 삭제 ===="
    find "${backup_dir}" -type f -mmin +360 -exec sh -c "ls -l {}; rm -f {}" \;

    echo -e "\n💿 PalWorld 백업 종료 ... 💿"
} > "${LOG_FILE_NM}"

# 텔레그램으로 알림 전송
TELEGRAM_FILE="/home/${HOST}/telegram/alarm.sh"
"${TELEGRAM_FILE}" "Master Server" "$(cat "${LOG_FILE_NM}")"

# 로그파일 삭제
rm -rf "${LOG_FILE_NM}"

▪️ 리소스 체크 스크립트

vim resource.sh

~/resource_check/script.sh

#!/usr/bin/bash

HOST='master'
TELEGRAM_FILE="/home/${HOST}/telegram/alarm.sh"

SWP_MEMORY=$(free -h | grep 'Swap' | awk '{print $4}')
MEMORY=$(free -h | grep 'Mem' | awk '{print $4}')
LOAD_AVERAGE=$(uptime | awk -F 'load average: ' '{print $2}')
MESSAGE="
MEMORY _> ${MEMORY}
SWAP MEMORY -> ${SWP_MEMORY}
uptime  -> ${LOAD_AVERAGE}"

"${TELEGRAM_FILE}" "${HOST}" "${MESSAGE}"

▪️ 알림 스크립트

  • 텔레그램 알림 스크립트

~/telegram/alarm.sh

#!/usr/bin/bash

if [[ ${#} -ne 2 ]]; then
	clear
	echo -e "Usage 👉 ${0} hostname message"
	exit 1
fi

HOST='master'
TELE_INFO_FILE="/home/${HOST}/telegram/token.env"
TELE_INFOS=($(awk -F '=' '{print $2}' "${TELE_INFO_FILE}"))
TOKEN="${TELE_INFOS[0]}"
CHAT_ID="${TELE_INFOS[1]}"
URL="https://api.telegram.org/bot${TOKEN}/sendMessage"

TEXT="[${1}] ${2}"
curl -s -d "chat_id=${CHAT_ID}&text=${TEXT}" ${URL} > /dev/null

▪️ 재부팅 스크립트

  • 메모리 점유율 80% 이상 시 서버 재부팅

bc 패키지 설치

sudo apt-get install -y bc

~/palworld/restart.sh

#!/usr/bin/bash

HOST='master'
YAML_FILE="/home/$HOST/palworld/docker-compose.yaml"
CONTAINER_NAME="palworld-server"
THRESHOLD=80

# 변수에 각각 삽입
read total used <<< $(free -m | awk '/Mem:/ {print $2 " " $3}')

# 현재 메모리 사용량 출력
echo "Current Memory usage: $(awk '/MemTotal/{total=$2}/MemAvailable/{available=$2} END {printf "%.0f", (total-available) / total * 100}' /proc/meminfo)%"

# 메모리 사용량 계산
usedPct=$(( used * 100 / total ))
if [[  "$usedPct" -gt $THRESHOLD ]]; then
    echo "Memory usage is above $THRESHOLD%."
    docker exec -i ${CONTAINER_NAME} rcon-cli "Broadcast Server_will_shut_down_in_5_seconds_for_maintance_Please_log_out"

    # 5초 대기
    sleep 5

    # 세이브
    docker exec -i $CONTAINER_NAME rcon-cli save

    # 서버 종료
    docker exec -i $CONTAINER_NAME rcon-cli "Broadcast Server_is_shutting_down_for_maintance"

    # 5초 대기
    sleep 5

    # 서버 재시작
    docker-compose -f "${YAML_FILE}" pull
    docker-compose -f "${YAML_FILE}" down
    docker-compose -f "${YAML_FILE}" up -d
else
    echo "Memory usage is below $THRESHOLD%."
fi

▪️ 밴 유저 삭제 스크립트

  • 밴 당한 유저의 세이브 데이터 자동 삭제 스크립트

~/palworld/deleteBanUserData.sh

#!/usr/bin/bash

HOST='master'
SAVE_PATH="/home/${HOST}/palworld/game/Pal/Saved/SaveGames"

echo -e "PlayerUID를 입력하세요 (여러개의 ID를 쓰는 경우 공백으로 구분)\n: "
while true; do 
    read -a Playeruids

    if [[ -z "${Playeruids}" ]]; then
        clear
        echo -e "PlayerUID 미입력됨.\n"
        continue
    fi
    break
done

clear
echo "플레이어 데이터 찾는 중 ..."
for playeruid in "${Playeruids[@]}"; do
  hex=$(printf "%X\n" "$playeruid" 2>/dev/null)
  if [[ "${hex}" == 0 ]]; then
    echo -e "${playeruid}: invalid octal number!"
    continue
  fi

  player_file="$(find "${SAVE_PATH}" -type f -iname "$hex*")"
  if [[ -z  "${player_file}" ]]; then
      echo -e "해당되는 플레이어를 찾지 못했습니다."
      continue
  fi

  echo "find! Ban Playeruid : ${hex}"
  echo -e "\nIs this really the ban user's file?\n👉 ${player_file}"
  read -p "y / Y " answer

  while true; do
      if [[ "${answer}" == 'y' ]] || [[ "${answer}" == 'Y' ]]; then
          echo -e "\nDeleting ban player's data ..."
          find "${SAVE_PATH}" -type f -iname "$hex*" -exec sh -c "rm -rf {}" \;
          exit 0
      else
          echo -e "With other data ..."
          break
      fi
  done
done

▪️ 서버 체크 스크립트

  • Palworld 컨테이너 alive 체크 및 서버 메모리 점유율 모니터링 결과를 디스코드 webhook으로 알림 전송

~/palworld-webhook/script.sh

#!/usr/bin/bash

#### Required variables
HOST="server01"
LOG_FILE="/home/${HOST}/discord-webhook/webhook.log"
WEBHOOK_URL_FILE="/home/${HOST}/discord-webhook/.webhook_url.txt"
DISCORD_WEBHOOK_URL=$(awk -F '=' '{print $2}' "$WEBHOOK_URL_FILE")
SERVER_NAME="palworld-server"
SERVER_ADDRESS="waytothem.store:5555"
PLAYERS="$(docker exec -i $SERVER_NAME rcon-cli ShowPlayers 2>/dev/null | awk 'NR > 1 {print}' | wc -l)"

if $(docker ps | grep $SERVER_NAME >/dev/null 2>&1); then
    # 도커 프로세스에 해당 서버가 있는 경우 & 서버가 실행 중인 경우
    {% raw %} if [[ $(docker inspect --format '{{.State.Status }}' "$SERVER_NAME") == 'running' ]];  {% endraw %} then
        SERVER_PROCESS=' 🟢  (온라인)'
    # 도커 프로세스에 해당 서버가 있는 경우 & 서버가 실행 중이지 않은 경우
    else
        SERVER_PROCESS=' 🟠  (중지됨)'
    fi
else
    SERVER_PROCESS=' 🔴  (오프라인)'
fi
#

#### 메모리 점유율 확인
MEMORY_USAGE=$(free | grep 'Mem' | awk '{print int($3/$2 * 100.0)}')

if [[ "$MEMORY_USAGE" -le 30 ]]; then
    color=2003199
elif [[ "$MEMORY_USAGE" -le 50 ]]; then
    color=16747520
elif [[ "$MEMORY_USAGE" -le 60 ]]; then
    color=16711680
else
    color=0
fi
#

#### 보낼 Embed 메시지 작성
MEMORY_MESSAGE="메모리 사용률: $MEMORY_USAGE%"
REBOOT_MESSAGE="메모리 점유율이 70% 이상 올라갈 시 자동 재부팅"
PLAYER_MESSAGE="접속 인원:  \`$PLAYERS/10\`"
SERVER_STATUS_MESSAGE="서버 상태: $SERVER_PROCESS"
SERVER_ADDRESS_MESSAGE="서버 주소:  \`$SERVER_ADDRESS\`"
BOUNDARY_MESSAGE="────────────────────────────"

#### JSON 데이터 인코딩
DATA=$( jq -n \
    --arg bm "$BOUNDARY_MESSAGE" \
    --arg mm "$MEMORY_MESSAGE" \
    --arg rm "$REBOOT_MESSAGE" \
    --arg pm "$PLAYER_MESSAGE" \
    --arg ssm "$SERVER_STATUS_MESSAGE" \
    --arg sam "$SERVER_ADDRESS_MESSAGE" \
    --arg c "$color" \
    '{ content: " ", embeds: [ { title: "💊 Server Status", color: $c, fields: [ { name: $bm, value: " ", inline: false }, { name: $ssm, value: " ", inline: false }, { name: $pm, value: " ", inline: false }, { name: $sam, value: " ", inline: false },{ name: $mm, value: " ", inline: false }, { name: "주의사항", value: $rm, inline: false } ] } ] }' )

# curl을 사용하여 웹훅 호출
curl -X POST -H "Content-Type: application/json" -d "$DATA" "$DISCORD_WEBHOOK_URL"

# curl의 반환 코드를 확인하여 로그 파일에 결과 기록
if [[ $? != 0 ]]; then
    echo "디스코드 메시지 전송 실패!" >> "$LOG_FILE"
else
    echo "디스코드 메시지 전송 성공!" >> "$LOG_FILE"
fi

▪️ 함수로 분리


~/palworld/PalworldManager.sh

#!/usr/bin/bash

#### Require variables
HOST='master'
#

#### 알림 스크립트
alarm() {
    if [[ ${#} -ne 2 ]]; then
        clear
        echo -e "Usage 👉 ${0} hostname message"
        exit 1
    fi

    HOST='master'
    TELE_INFO_FILE="/home/${HOST}/telegram/token.env"
    TELE_INFOS=($(awk -F '=' '{print $2}' "${TELE_INFO_FILE}"))
    TOKEN="${TELE_INFOS[0]}"
    CHAT_ID="${TELE_INFOS[1]}"
    URL="https://api.telegram.org/bot${TOKEN}/sendMessage"

    TEXT="[${1}] ${2}"
    curl -s -d "chat_id=${CHAT_ID}&text=${TEXT}" ${URL} > /dev/null
}
####

#### 백업 함수
backup() {
    SERVER_NAME="palworld-server"

    if $(docker ps | grep $SERVER_NAME >/dev/null 2>&1); then
        {% raw %} if [[ $(docker inspect --format '{{.State.Status }}' "$SERVER_NAME") == 'running' ]]; then {% endraw %}
            echo "$SERVER_NAME is running"
        # 도커 프로세스에 해당 서버가 있는 경우 & 서버가 실행 중이지 않은 경우
        else
            echo "$SERVER_NAME is paused"
            exit 1
        fi
    else
        echo "$SERVER_NAME is stoped"
        exit 1
    fi

    # 압축할 디렉토리
    source_dir="/home/${HOST}/palworld/game/Pal/Saved/SaveGames"

    # 압축 파일이 저장될 디렉토리
    backup_dir="/tmp/palworld/backup"

    # 압축 파일 이름
    DATE="$(date +'%Y%m%d_%H%M')"
    backup_filename="palworld_backup_${DATE}.tar.gz"

    # 백업 디렉토리 없는 경우 생성
    if [[ ! -d "${backup_dir}" ]]; then
        mkdir -p "${backup_dir}"
    fi

    # 로그 파일 생성
    LOG_FILE_NM="/tmp/backup.log"
    touch "${LOG_FILE_NM}"

    {
        echo -e "\n💿 PalWorld 백업 시작 ... 💿"

        # 디렉토리를 gzip 압축
        tar -czf "${backup_dir}/${backup_filename}" "${source_dir}" 1>/dev/null 2>&1
        NAME=$(ls -lh "${backup_dir}/${backup_filename}" | awk '{print $9}')
        SIZE=$(ls -lh "${backup_dir}/${backup_filename}" | awk '{print $5}')
        
        echo "==== 백업 파일 정보 ===="
        echo "파일명 : ${NAME}"
        echo "파일 크기 : ${SIZE}"
        echo

        echo "==== 이전 백업파일 삭제 ===="
        find "${backup_dir}" -type f -mmin +360 -exec sh -c "ls -l {}; rm -f {}" \;

        echo -e "\n💿 PalWorld 백업 종료 ... 💿"
    } > "${LOG_FILE_NM}"

    # 텔레그램으로 알림 전송
    alarm "Master Server" "$(cat "${LOG_FILE_NM}")"

    # 로그파일 삭제
    rm -rf "${LOG_FILE_NM}"
    }
####

#### 리소스 체크 함수
resource_check() {
    SWP_MEMORY=$(free -h | grep 'Swap' | awk '{print $4}')
    MEMORY=$(free -h | grep 'Mem' | awk '{print $4}')
    LOAD_AVERAGE=$(uptime | awk -F 'load average: ' '{print $2}')
    MESSAGE="
    MEMORY _> ${MEMORY}
    SWAP MEMORY -> ${SWP_MEMORY}
    uptime  -> ${LOAD_AVERAGE}"

    alarm "${HOST}" "${MESSAGE}"
}
####

#### 재부팅 스크립트
reboot() {
    YAML_FILE="/home/$HOST/palworld/docker-compose.yaml"
    CONTAINER_NAME="palworld-server"
    THRESHOLD=80

    # 변수에 각각 삽입
    read total used <<< $(free -m | awk '/Mem:/ {print $2 " " $3}')

    # 현재 메모리 사용량 출력
    echo "Current Memory usage: $(awk '/MemTotal/{total=$2}/MemAvailable/{available=$2} END {printf "%.0f", (total-available) / total * 100}' /proc/meminfo)%"

    # 메모리 사용량 계산
    usedPct=$(( used * 100 / total ))
    if [[  "$usedPct" -gt $THRESHOLD ]]; then
        echo "Memory usage is above $THRESHOLD%."
        docker exec -i ${CONTAINER_NAME} rcon-cli "Broadcast Server_will_shut_down_in_5_seconds_for_maintance_Please_log_out"

        # 5초 대기
        sleep 5

        # 세이브
        docker exec -i $CONTAINER_NAME rcon-cli save

        # 서버 종료
        docker exec -i $CONTAINER_NAME rcon-cli "Broadcast Server_is_shutting_down_for_maintance"

        # 5초 대기
        sleep 5

        # 서버 재시작
        docker-compose -f "${YAML_FILE}" pull
        docker-compose -f "${YAML_FILE}" down
        docker-compose -f "${YAML_FILE}" up -d
    else
        echo "Memory usage is below $THRESHOLD%."
    fi
}
####

# man logic to call functions based on passed argument
case "$1" in
    backup)
        backup
        ;;
    resource)
        resource_check
        ;;
    restart)
        reboot
        ;;
    *)
        echo -e "$0 {backup|resource|restart}"
        exit 1
        ;;
esac

◾️ crontab 등록

1. 스크립트 각각 등록한 경우

# 등록된 cron job 출력
crontab -l

# 5분마다 rcon으로 조회된 서버 player 목록 파일로 생성
*/5  * * * *  bash /home/master/rconUserAlarm/set-rcon-players.sh >/dev/null 2>&1

# 10분마다 재부팅 스크립트 실행
*/10 * * * *  bash /home/master/palworld/restart.sh >> /home/master/backup/restart.log 2>&1

# 30분마다 백업 스크립트 실행
*/30 * * * * bash /home/master/backup/script.sh >> /home/master/backup/backup.log 2>&1

# 30분마다 리소스 체크 스크립트 실행
*/30 * * * * bash /home/master/resource_alarm/alarm.sh >> /home/master/backup/resource.log 2>&1

2. 함수 기반으로 기능 분리하여 하나의 스크립트로 등록한 경우

# 등록된 cron job 출력
crontab -l

# 5분마다 rcon으로 조회된 서버 player 목록 파일로 생성
*/5  * * * *  bash /home/master/rconUserAlarm/set-rcon-players.sh >/dev/null 2>&1

# 10분마다 재부팅 스크립트 실행
*/10 * * * * bash /home/master/palworld/PalworldManager.sh restart >> /home/master/backup/restart.log 2>&1

# 30분마다 백업 스크립트 실행
*/30 * * * * bash /home/master/palworld/PalworldManager.sh backup >> /home/master/backup/backup.log 2>&1

# 30분마다 리소스 체크 스크립트 실행
*/30 * * * * bash /home/master/palworld/PalworldManager.sh resource >> /home/master/backup/resource.log 2>&1

▪️ 실행 결과

image


Loading script...