#!/usr/bin/env bash set -Eeuo pipefail script_dir=$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) config_file="${script_dir}/cfsv2_manual.conf" dry_run=0 # User-configurable directory containing the YYYYMMDD download directories. # It can also be overridden from the environment when invoking this script. #OUTPUT_DIR="${OUTPUT_DIR:-/home/tanpv/Progs/CLI/Check_data/data/CFS_Ope/00}" OUTPUT_DIR="${OUTPUT_DIR:-/home/nampq/data/CFS_Ope/00}" usage() { cat <<'EOF' Usage: ./ope_download_manual.sh [options] YEAR MONTH DAY [HOUR] Examples: ./ope_download_manual.sh 2026 08 21 ./ope_download_manual.sh 2026 08 21 00 ./ope_download_manual.sh --dry-run 2026 08 21 Options: --config FILE Read an alternative configuration file. --dry-run Validate and show URLs/paths without creating or downloading. -h, --help Show this help. The default HOUR is configured as 00. Output is stored below this script's directory unless CFS_DATA_ROOT/CFS_LOG_ROOT are explicitly configured otherwise. EOF } die() { printf 'ERROR: %s\n' "$*" >&2 exit 1 } while (($#)); do case "$1" in --config) (($# >= 2)) || die "--config requires a file" config_file=$2 shift 2 ;; --dry-run) dry_run=1 shift ;; -h|--help) usage exit 0 ;; --) shift break ;; -* ) die "unknown option: $1" ;; *) break ;; esac done (($# == 3 || $# == 4)) || { usage >&2 exit 2 } year=$1 month=$2 day=$3 hour=${4:-} [[ -r "$config_file" ]] || die "configuration is not readable: $config_file" CFS_PACKAGE_DIR=$script_dir # shellcheck source=cfsv2_manual.conf source "$config_file" hour=${hour:-$CFS_DEFAULT_HOUR} [[ $year =~ ^[0-9]{4}$ ]] || die "YEAR must contain four digits" [[ $month =~ ^[0-9]{1,2}$ ]] || die "MONTH must contain one or two digits" [[ $day =~ ^[0-9]{1,2}$ ]] || die "DAY must contain one or two digits" [[ $hour =~ ^[0-9]{1,2}$ ]] || die "HOUR must contain one or two digits" printf -v month '%02d' "$((10#$month))" printf -v day '%02d' "$((10#$day))" printf -v hour '%02d' "$((10#$hour))" ymd="${year}${month}${day}" ymdh="${ymd}${hour}" normalized=$(date -u -d "${year}-${month}-${day} ${hour}:00:00" +%Y%m%d%H 2>/dev/null) || die "invalid UTC date: ${year}-${month}-${day} ${hour}:00" [[ $normalized == "$ymdh" ]] || die "invalid UTC date: ${year}-${month}-${day} ${hour}:00" case " $CFS_ALLOWED_HOURS " in *" $hour "*) ;; *) die "HOUR $hour is not in CFS_ALLOWED_HOURS: $CFS_ALLOWED_HOURS" ;; esac for command_name in curl date grep realpath sort wc wget; do command -v "$command_name" >/dev/null 2>&1 || die "required command not found: $command_name" done package_root=$(realpath -m -- "$script_dir") log_root=$(realpath -m -- "$CFS_LOG_ROOT") case "$log_root/" in "$package_root"/*) CFS_LOG_ROOT=$log_root ;; *) die "CFS_LOG_ROOT must stay below package directory: $package_root" ;; esac OUTPUT_DIR=$(realpath -m -- "$OUTPUT_DIR") [[ $OUTPUT_DIR == /* ]] || die "OUTPUT_DIR must resolve to an absolute path" remote_dir="${CFS_BASE_URL}/cfs.${ymd}/${hour}/${CFS_REMOTE_SUBDIR}" cycle_output_dir="${OUTPUT_DIR}/${ymd}" log_file="${CFS_LOG_ROOT}/cfs.${ymdh}.files" printf '%s\n' \ "CFSv2 manual download" \ " cycle : $ymdh UTC" \ " remote : $remote_dir/" \ " output : $cycle_output_dir" \ " file list : $log_file" \ " model mode : $MODEL_MODE" if ((dry_run)); then printf 'DRY RUN: validation completed; no directory or file was created.\n' exit 0 fi [[ $MODEL_MODE == none ]] || die "review package supports MODEL_MODE=none only" mkdir -p -- "$cycle_output_dir" "$CFS_LOG_ROOT" curl_options=( --fail --silent --show-error --location --connect-timeout "$CFS_CONNECT_TIMEOUT" --retry "$CFS_RETRY_COUNT" --retry-delay "$CFS_RETRY_DELAY" ) wget_options=( --continue --timeout="$CFS_CONNECT_TIMEOUT" --tries="$CFS_RETRY_COUNT" ) if [[ $CFS_INSECURE_TLS == 1 ]]; then curl_options+=(--insecure) wget_options+=(--no-check-certificate) fi listing=$(curl "${curl_options[@]}" "$remote_dir/") || die "cannot read remote directory: $remote_dir/" printf '%s\n' "$listing" | grep -oE "$CFS_FILE_REGEX" | sort -u > "$log_file" || true file_count=$(wc -l < "$log_file") ((file_count > 0)) || die "no flxf/pgbf GRIB2 files found at $remote_dir/" printf 'Remote files found: %d\n' "$file_count" # Treat a cycle as complete only when every file currently advertised by the # remote directory already exists locally and is non-empty. A partial cycle # still falls through to the normal loop so that only missing files are fetched. all_files_downloaded=1 while IFS= read -r filename; do if [[ ! -s "${cycle_output_dir}/${filename}" ]]; then all_files_downloaded=0 break fi done < "$log_file" if ((all_files_downloaded)); then printf 'Data of %s-%s-%s-%s have already been downloaded.\n' \ "$year" "$month" "$day" "$hour" exit 0 fi downloaded=0 skipped=0 failed=0 while IFS= read -r filename; do [[ $filename =~ ^(flxf|pgbf)[A-Za-z0-9._-]+\.grb2$ ]] || { printf '[REJECT] Unexpected filename: %s\n' "$filename" >&2 ((failed += 1)) continue } destination="${cycle_output_dir}/${filename}" if [[ -s "$destination" ]]; then printf '[SKIP] %s\n' "$filename" ((skipped += 1)) continue fi printf '[GET ] %s\n' "$filename" if wget "${wget_options[@]}" "$remote_dir/$filename" -O "$destination"; then ((downloaded += 1)) else printf '[FAIL] %s\n' "$filename" >&2 ((failed += 1)) fi done < "$log_file" printf '%s\n' \ "Download summary" \ " downloaded : $downloaded" \ " skipped : $skipped" \ " failed : $failed" \ " output : $cycle_output_dir" ((failed == 0)) || exit 1