728x90

가끔씩 필요해서 찾게 되는 것이라 정리해 봅니다.

 

1. sftp-config.sh (설정 파일)
용량 비교 및 상세 체크를 위해 SSH 접속 정보를 그대로 사용합니다.

더보기
# sftp-config.sh
# SFTP 서버 접속 정보 및 경로 설정

# [필수] SFTP 서버 정보
SFTP_HOST="sftp.example.com"
SFTP_PORT="22"
SFTP_USER="root"

# [선택] RSA 개인키 경로 (기본: /root/.ssh/id_rsa)
IDENTITY_FILE="/root/.ssh/id_rsa"

# 재시도 설정
MAX_RETRIES=2
BACKOFF_BASE=2

# 로그 및 경로 설정
LOG_BASE_DIR="/root/sftp_logs"       # 상세 실행 로그
RESULT_LOG_DIR="/root/sftp_results" # 운영용 요약 로그
DB_READY_LOG_DIR="/root/sftp_db_logs" # DB 적재용 정제 로그 (JSONL)
TARGET_LIST_DIR="/root/sftp_targets"

# 동일 용량 파일 스킵 여부 (YES: 스킵 및 로그 남김, NO: 덮어쓰기)
SKIP_SAME_SIZE="YES"

2. sftp_download.sh (고도화된 전체 소스)
이 스크립트는 각 파일의 상태를 4가지 상태(SUCCESS, NOT_FOUND, SKIPPED_SAME, FAILED)로 구분하여 로그를 남깁니다.

더보기
#!/usr/bin/env bash
# sftp_download.sh - DB 연동 최적화 및 상세 로깅 버전

set -uo pipefail

BASE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG_FILE="${BASE_DIR}/sftp-config.sh"
MODE=""          
REMOTE_BASE=""   
PATTERN=""       
TARGET_LIST=""   
LOCAL_DIR=""     

usage() {
  cat <<EOF
Usage: $(basename "$0") -M <mode> -r <remote_base> -d <local_abs_path> [options]
Modes: tree | list
EOF
}

# 1. 텍스트 로그 (운영자 확인용)
log() {
  local level="$1"; shift
  local msg="$*"
  echo "$(date '+%Y-%m-%d %H:%M:%S') [${level}] ${msg}" | tee -a "${LOG_FILE}"
}

# 2. 정제 로그 (DB 적재 프로그램용 - JSONL)
record_db_log() {
  local status="$1"
  local file_path="$2"
  local size="${3:-0}"
  local msg="${4:-""}"
  local ts
  ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ") # ISO 8601 표준

  # DB 프로그램에서 파싱하기 좋게 한 줄의 JSON으로 기록
  printf '{"ts":"%s", "status":"%s", "file":"%s", "size":%d, "msg":"%s"}\n' \
    "$ts" "$status" "$file_path" "$size" "$msg" >> "${DB_LOG}"
}

while getopts "M:r:p:f:d:c:h" opt; do
  case "${opt}" in
    M) MODE="${OPTARG}" ;;
    r) REMOTE_BASE="${OPTARG}" ;;
    p) PATTERN="${OPTARG}" ;;
    f) TARGET_LIST="${OPTARG}" ;;
    d) LOCAL_DIR="${OPTARG}" ;;
    c) CONFIG_FILE="${OPTARG}" ;;
    h) usage; exit 0 ;;
    *) usage; exit 1 ;;
  esac
done

if [[ ! -f "${CONFIG_FILE}" ]]; then
  echo "설정 파일을 찾을 수 없습니다: ${CONFIG_FILE}" >&2
  exit 1
fi

source "${CONFIG_FILE}"

# 디렉토리 및 로그 파일 준비
mkdir -p "${LOG_BASE_DIR}" "${RESULT_LOG_DIR}" "${DB_READY_LOG_DIR}" "${LOCAL_DIR}"
LOG_FILE="${LOG_BASE_DIR}/sftp_detail_$(date +%Y%m%d).log"
DB_LOG="${DB_READY_LOG_DIR}/transfer_data_$(date +%Y%m%d).jsonl"
SSH_OPTS="-i ${IDENTITY_FILE} -p ${SFTP_PORT} -o BatchMode=yes -o StrictHostKeyChecking=yes -o ServerAliveInterval=60"

download_file() {
  local rel_path="$1"
  local remote_p="${REMOTE_BASE}/${rel_path}"
  local local_p="${LOCAL_DIR}/${rel_path}"
  
  # 1. 원격 파일 정보 획득
  local r_size
  r_size=$(ssh ${SSH_OPTS} "${SFTP_USER}@${SFTP_HOST}" "stat -c%s '${remote_p}'" 2>/dev/null || echo "NOT_FOUND")

  if [[ "${r_size}" == "NOT_FOUND" ]]; then
    log WARN "파일 없음: ${rel_path}"
    record_db_log "NOT_FOUND" "${rel_path}" 0 "Remote file not found"
    return 1
  fi

  # 2. 로컬 파일과 용량 비교 (중복 체크)
  if [[ -f "${local_p}" ]]; then
    local l_size
    l_size=$(stat -c%s "${local_p}")
    if [[ "${r_size}" == "${l_size}" && "${SKIP_SAME_SIZE}" == "YES" ]]; then
      log INFO "스킵(동일 용량): ${rel_path}"
      record_db_log "SKIPPED" "${rel_path}" "${r_size}" "Same size skip"
      return 0
    fi
  fi

  # 3. 실제 SFTP 전송
  mkdir -p "$(dirname "${local_p}")"
  if sftp -b - -P "${SFTP_PORT}" -i "${IDENTITY_FILE}" -C -o BatchMode=yes "${SFTP_USER}@${SFTP_HOST}" <<EOF >> "${LOG_FILE}" 2>&1
get "${remote_p}" "${local_p}"
quit
EOF
  then
    log INFO "성공: ${rel_path}"
    record_db_log "SUCCESS" "${rel_path}" "${r_size}" "Transfer complete"
  else
    log ERROR "실패: ${rel_path}"
    record_db_log "FAILED" "${rel_path}" "${r_size}" "SFTP error during transfer"
    return 1
  fi
}

log INFO "=== 작업 시작 (Mode: ${MODE}) ==="

if [[ "${MODE}" == "list" ]]; then
  # 리스트 파일이 지정되지 않았다면 TARGET_LIST_DIR의 첫 번째 파일 사용
  [[ -z "${TARGET_LIST}" ]] && TARGET_LIST=$(find "${TARGET_LIST_DIR}" -type f | head -n 1)
  
  if [[ -f "${TARGET_LIST}" ]]; then
    while IFS= read -r line || [[ -n "$line" ]]; do
      [[ -z "$line" || "$line" =~ ^# ]] && continue
      download_file "${line}"
    done < "${TARGET_LIST}"
  else
    log ERROR "리스트 파일을 찾을 수 없습니다."
  fi

elif [[ "${MODE}" == "tree" ]]; then
  # 원격지 패턴 검색 후 목록 기반 다운로드
  log INFO "원격지 목록 조회 중..."
  file_list=$(ssh ${SSH_OPTS} "${SFTP_USER}@${SFTP_HOST}" "find '${REMOTE_BASE}' -type f -name '${PATTERN}'" | sed "s|${REMOTE_BASE}/||")
  
  for f in ${file_list}; do
    download_file "$f"
  done
fi

log INFO "=== 작업 종료 (결과: ${DB_LOG}) ==="

3. 스크립트의 특징 (운영 강점)
상세한 상태 분류:
SUCCESS: 파일이 성공적으로 전송됨.
NOT_FOUND: 원격 서버에 대상 파일이 아예 없음 (경로 오타 등).
SKIPPED: 이미 로컬에 같은 용량의 파일이 있어 전송을 생략함 (네트워크 자원 절약).
FAILED: 파일은 있으나 권한 문제, 네트워크 끊김 등으로 가져오지 못함.
용량 기반 검증: 단순히 파일명만 보는 게 아니라 stat 명령으로 용량을 비교하여 '정상적으로 가져온 상태'를 확신할 수 있습니다.
가독성 높은 요약 로그: RESULT_LOG 파일만 열어보면 어떤 파일이 어떤 이유로 처리되었는지 한눈에 알 수 있어 운영이 편리합니다.

더보기

 

  • DB 적재용 로그 샘플 (transfer_data_YYYYMMDD.jsonl):
  • JSON
     
    {"ts":"2026-03-18T00:50:01Z", "status":"SUCCESS", "file":"daily/data.csv", "size":1048, "msg":"Transfer complete"}
    {"ts":"2026-03-18T00:50:03Z", "status":"SKIPPED", "file":"backup/db.sql", "size":50020, "msg":"Same size skip"}
    
  • 후속 프로그램과의 연계:
    • 후속 프로그램(Java/Python 등)이 실행될 때 DB_READY_LOG_DIR 내의 파일을 한 줄씩 읽습니다.
    • status가 SUCCESS면 DB의 전송 완료 테이블에 INSERT 합니다.
    • status가 FAILED면 관리자 알림 테이블에 넣거나 재시도 큐에 넣습니다.
  • 성능 최적화:
    • -C 옵션을 통해 다량의 파일 전송 시 데이터를 압축하여 대역폭을 절약합니다.
    • ServerAliveInterval을 설정하여 대용량 파일 전송 중 연결 끊김을 방지합니다.

 

 

4. 사용 예제
Tree 모드 (패턴)

./sftp_download.sh -M tree -r /data/logs -p "*.csv" -d /root/downloads

동작: /data/logs에서 .csv를 찾아 리스트를 만든 후, 하나씩 용량 체크하며 가져옵니다.

List 모드 (파일명 나열)

./sftp_download.sh -M list -r /data/origin -f targets.txt -d /root/downloads

 

급하게 정리해봅니다.

728x90

+ Recent posts