sh数据同步备份脚本

生成目录文件并写入文件tmpfile.txt

powershell
Get-ChildItem -Path "." -Recurse -File | ForEach-Object { $_.FullName.Substring((Get-Item ".").FullName.Length + 1).Replace('\', '/') } | Out-File -FilePath "tmpfile.txt" -Encoding UTF8

bash
find . -type f -printf '%P\n' | sed 's|\\|/|g' > tmpfile.txt

linux执行备份脚本

#!/bin/bash

# 安全模式:遇到未定义变量或管道失败时退出
set -uo pipefail

show_usage() {
    cat <<EOF
用法: $0 <文件列表> <源目录> <备份目录>

参数说明:
    文件列表   : 包含相对路径的文本文件(一行一个,如 common/file.php)
    源目录     : 要从中复制文件的根目录(必须存在且为目录)
    备份目录   : 目标备份目录(必须不存在,或存在但为空)

示例:
    $0 tmpfile.txt /www/wwwroot/app /backup/app_20251208

注意:
    - 自动跳过源中不存在的文件
    - 自动按原目录结构创建目标路径
    - 备份目录非空时拒绝执行,防止误覆盖
    - 自动清理文件列表中的 BOM 和 Windows 换行符(\r)
EOF
}

# === 参数校验 ===
if [[ $# -ne 3 ]]; then
    echo "❌ 错误: 需要 3 个参数。" >&2
    show_usage
    exit 1
fi

FILE_LIST="$1"
SOURCE_ROOT="$2"
DEST_ROOT="$3"

# 标准化路径(解析符号链接,转为绝对路径)
if ! SOURCE_ROOT="$(realpath "$SOURCE_ROOT" 2>/dev/null)" || [[ -z "$SOURCE_ROOT" ]]; then
    echo "❌ 错误: 无法解析源目录路径: $SOURCE_ROOT" >&2
    exit 1
fi

if ! DEST_ROOT="$(realpath -m "$DEST_ROOT" 2>/dev/null)" || [[ -z "$DEST_ROOT" ]]; then
    echo "❌ 错误: 无法解析备份目录路径: $DEST_ROOT" >&2
    exit 1
fi

# === 文件与目录存在性校验 ===
if [[ ! -f "$FILE_LIST" ]]; then
    echo "❌ 错误: 文件列表不存在: $FILE_LIST" >&2
    exit 1
fi

if [[ ! -d "$SOURCE_ROOT" ]]; then
    echo "❌ 错误: 源目录不存在或不是目录: $SOURCE_ROOT" >&2
    exit 1
fi

# === 自动清理文件列表:去除 UTF-8 BOM 和 Windows \r,并回写到原文件 ===
echo "ℹ️  清理文件列表中的 BOM 和 Windows 换行符..." >&2
{
    # 读取内容,去除 BOM(仅第一行)和所有 \r
    sed '1s/^\xEF\xBB\xBF//' "$FILE_LIST" | tr -d '\r'
} > "${FILE_LIST}.clean" && mv "${FILE_LIST}.clean" "$FILE_LIST"

# === 备份目录安全检查 ===
if [[ -e "$DEST_ROOT" ]]; then
    if [[ ! -d "$DEST_ROOT" ]]; then
        echo "❌ 错误: 备份路径已存在但不是目录: $DEST_ROOT" >&2
        exit 1
    fi
    if [[ -n "$(ls -A "$DEST_ROOT" 2>/dev/null)" ]]; then
        echo "❌ 错误: 备份目录非空!为防止数据覆盖,拒绝操作: $DEST_ROOT" >&2
        exit 1
    fi
else
    echo "ℹ️  备份目录不存在,将自动创建: $DEST_ROOT"
fi

# === 读取并预处理文件列表 ===
declare -a rel_paths=()
while IFS= read -r line || [[ -n "$line" ]]; do
    # 跳过空行和注释(以 # 开头,允许前导空格)
    if [[ -n "$line" ]] && ! [[ "$line" =~ ^[[:space:]]*# ]]; then
        rel_paths+=("$line")
    fi
done < "$FILE_LIST"

total="${#rel_paths[@]}"

if (( total == 0 )); then
    echo "⚠️  警告: 文件列表中没有有效路径(空文件或全是注释/空行)。" >&2
    echo
    echo "📊 操作完成!"
    echo "  总文件数: 0"
    echo "  成功:     0"
    echo "  失败:     0"
    echo "✅ 无操作完成。"
    exit 0
fi

# === 初始化计数器 ===
success=0
failed=0
declare -a failed_list

# === 主处理循环 ===
for rel_path in "${rel_paths[@]}"; do
    src="$SOURCE_ROOT/$rel_path"
    dest="$DEST_ROOT/$rel_path"
    dest_dir="$(dirname "$dest")"

    if [[ -f "$src" ]]; then
        if mkdir -p "$dest_dir" 2>/dev/null && cp "$src" "$dest" 2>/dev/null; then
            ((success++))
        else
            ((failed++))
            failed_list+=("$rel_path")
            echo "⚠️  处理失败: $rel_path" >&2
        fi
    else
        ((failed++))
        failed_list+=("$rel_path")
        echo "⚠️  源文件不存在或非普通文件: $rel_path" >&2
    fi
done

# === 输出汇总报告 ===
echo
echo "📊 操作完成!"
echo "  总文件数: $total"
echo "  成功:     $success"
echo "  失败:     $failed"

if (( failed > 0 )); then
    echo
    echo "❌ 以下 $failed 个文件处理失败:"
    for fp in "${failed_list[@]}"; do
        printf "  - %s\n" "$fp"
    done
    exit 1
else
    echo "✅ 所有文件备份成功!"
    exit 0
fi
冀ICP备2021025979号-1