Last active
March 19, 2026 07:20
-
-
Save xxnuo/6392dc6df4178cdedeb3798d485b04bc to your computer and use it in GitHub Desktop.
glink 快速移动文件到仓库盘并创建软链接的工具
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| VERSION="1.2.0" | |
| CURRENT_USER="$(id -un)" | |
| HOME_BASE="$(eval echo "~$CURRENT_USER")" | |
| REPO_BASE="/media/$CURRENT_USER/data" | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| CYAN='\033[0;36m' | |
| NC='\033[0m' | |
| log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } | |
| log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } | |
| log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } | |
| log_step() { echo -e "${CYAN}[STEP]${NC} $*"; } | |
| NEED_SUDO=false | |
| SUDO="" | |
| check_sudo() { | |
| if [[ "$HAS_SUDO" != true ]]; then | |
| return | |
| fi | |
| if [[ "$NEED_SUDO" == true ]]; then | |
| return | |
| fi | |
| local check_path="$1" | |
| if [[ ! -e "$check_path" ]]; then | |
| return | |
| fi | |
| if [[ ! -r "$check_path" || ! -w "$check_path" ]]; then | |
| NEED_SUDO=true | |
| return | |
| fi | |
| local parent="$(dirname "$check_path")" | |
| if [[ ! -w "$parent" ]]; then | |
| NEED_SUDO=true | |
| return | |
| fi | |
| if [[ -d "$check_path" ]]; then | |
| local owner | |
| owner="$(stat -c '%u' "$check_path")" | |
| if [[ "$owner" != "$(id -u)" ]]; then | |
| NEED_SUDO=true | |
| return | |
| fi | |
| if timeout 3 find "$check_path" -maxdepth 3 -not -user "$(id -u)" -print -quit 2>/dev/null | grep -q .; then | |
| NEED_SUDO=true | |
| fi | |
| fi | |
| } | |
| acquire_sudo() { | |
| if [[ "$NEED_SUDO" != true ]]; then | |
| return | |
| fi | |
| SUDO="sudo" | |
| log_warn "操作涉及需要 root 权限的文件,需要提权" | |
| if ! sudo -v; then | |
| log_error "无法获取 sudo 权限" | |
| exit 1 | |
| fi | |
| } | |
| usage() { | |
| cat <<EOF | |
| glink v${VERSION} - 将文件/文件夹移动到仓库盘并创建软链接 | |
| 用法: $(basename "$0") <源路径> [目标路径] [选项] | |
| 参数: | |
| <源路径> 要移动的文件或文件夹(将被替换为软链接) | |
| [目标路径] 仓库盘上的目标位置(可选) | |
| 省略时自动将 ${HOME_BASE} 替换为 ${REPO_BASE} | |
| 选项: | |
| -n, --dry-run 仅模拟运行,不实际操作 | |
| -f, --force 跳过确认提示 | |
| -h, --help 显示帮助 | |
| -v, --version 显示版本 | |
| 示例: | |
| $(basename "$0") ~/large-folder | |
| -> 自动映射到 ${REPO_BASE}/large-folder | |
| $(basename "$0") ~/projects/work/data | |
| -> 自动映射到 ${REPO_BASE}/projects/work/data | |
| $(basename "$0") ~/big-file.tar.gz /mnt/other/big-file.tar.gz | |
| $(basename "$0") -f ~/data | |
| EOF | |
| exit 0 | |
| } | |
| DRY_RUN=false | |
| FORCE=false | |
| SOURCE="" | |
| DEST="" | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| -n|--dry-run) DRY_RUN=true; shift ;; | |
| -f|--force) FORCE=true; shift ;; | |
| -h|--help) usage ;; | |
| -v|--version) echo "glink v${VERSION}"; exit 0 ;; | |
| -*) log_error "未知选项: $1"; usage ;; | |
| *) | |
| if [[ -z "$SOURCE" ]]; then | |
| SOURCE="$1" | |
| elif [[ -z "$DEST" ]]; then | |
| DEST="$1" | |
| else | |
| log_error "参数过多" | |
| usage | |
| fi | |
| shift | |
| ;; | |
| esac | |
| done | |
| if [[ -z "$SOURCE" ]]; then | |
| log_error "必须提供源路径" | |
| usage | |
| fi | |
| command -v rsync >/dev/null 2>&1 || { log_error "rsync 未安装 (强依赖项缺失)"; exit 1; } | |
| HAS_SUDO=false | |
| if command -v sudo >/dev/null 2>&1 && [[ "$(id -u)" != "0" ]]; then | |
| HAS_SUDO=true | |
| fi | |
| TRASH_CMD="" | |
| if command -v trash >/dev/null 2>&1; then | |
| TRASH_CMD="trash" | |
| elif command -v trash-put >/dev/null 2>&1; then | |
| TRASH_CMD="trash-put" | |
| fi | |
| SOURCE="$(realpath -s "$SOURCE")" | |
| if [[ -z "$DEST" ]]; then | |
| if [[ "$SOURCE" != "$HOME_BASE"/* ]]; then | |
| log_error "源路径不在 $HOME_BASE 下,无法自动推导目标路径,请手动指定" | |
| exit 1 | |
| fi | |
| REL_PATH="${SOURCE#$HOME_BASE/}" | |
| DEST="${REPO_BASE}/${REL_PATH}" | |
| log_info "自动推导目标路径: $DEST" | |
| else | |
| DEST="$(realpath -s "$DEST")" | |
| fi | |
| if [[ ! -d "$REPO_BASE" ]]; then | |
| log_error "仓库盘未挂载: $REPO_BASE" | |
| exit 1 | |
| fi | |
| if [[ ! -e "$SOURCE" ]]; then | |
| log_error "源路径不存在: $SOURCE" | |
| exit 1 | |
| fi | |
| if [[ -L "$SOURCE" ]]; then | |
| log_error "源路径已经是软链接: $SOURCE" | |
| exit 1 | |
| fi | |
| if [[ "$SOURCE" == "$DEST" ]]; then | |
| log_error "源路径和目标路径相同" | |
| exit 1 | |
| fi | |
| if [[ "$DEST" == "$SOURCE"/* ]]; then | |
| log_error "目标路径不能是源路径的子目录" | |
| exit 1 | |
| fi | |
| if [[ "$SOURCE" == "$DEST"/* ]]; then | |
| log_error "源路径不能是目标路径的子目录" | |
| exit 1 | |
| fi | |
| if [[ -e "$DEST" ]]; then | |
| if [[ -d "$SOURCE" && -d "$DEST" ]]; then | |
| log_warn "目标目录已存在: $DEST" | |
| log_warn "rsync 将进行增量同步" | |
| elif [[ -f "$SOURCE" && -f "$DEST" ]]; then | |
| log_warn "目标文件已存在: $DEST" | |
| log_warn "rsync 将进行校验并按需更新" | |
| else | |
| log_error "源和目标类型不一致(一个是文件,一个是目录)" | |
| exit 1 | |
| fi | |
| fi | |
| check_sudo "$SOURCE" | |
| SOURCE_PARENT="$(dirname "$SOURCE")" | |
| if [[ ! -w "$SOURCE_PARENT" ]]; then | |
| NEED_SUDO=true | |
| fi | |
| SOURCE_SIZE=$($SUDO du -sh "$SOURCE" 2>/dev/null | cut -f1) | |
| IS_DIR=false | |
| [[ -d "$SOURCE" ]] && IS_DIR=true | |
| echo "" | |
| log_info "操作摘要:" | |
| echo " 源路径: $SOURCE" | |
| echo " 目标路径: $DEST" | |
| echo " 类型: $( $IS_DIR && echo '文件夹' || echo '文件' )" | |
| echo " 大小: $SOURCE_SIZE" | |
| if [[ -n "$TRASH_CMD" ]]; then | |
| echo " 删除: 将使用 $TRASH_CMD 移至回收站" | |
| else | |
| echo -e " 删除: ${YELLOW}直接永久删除 (rm)${NC}" | |
| fi | |
| $NEED_SUDO && echo -e " 权限: ${YELLOW}需要 sudo${NC}" | |
| $DRY_RUN && echo -e " 模式: ${YELLOW}模拟运行${NC}" | |
| echo "" | |
| if [[ "$FORCE" != true && "$DRY_RUN" != true ]]; then | |
| read -rp "确认执行?(y/N) " confirm | |
| if [[ "$confirm" != [yY] && "$confirm" != [yY][eE][sS] ]]; then | |
| log_info "已取消" | |
| exit 0 | |
| fi | |
| fi | |
| acquire_sudo | |
| DEST_DIR="$(dirname "$DEST")" | |
| if [[ ! -d "$DEST_DIR" ]]; then | |
| log_step "创建目标父目录: $DEST_DIR" | |
| if [[ "$DRY_RUN" != true ]]; then | |
| $SUDO mkdir -p "$DEST_DIR" | |
| fi | |
| fi | |
| RSYNC_OPTS=( | |
| --archive | |
| --hard-links | |
| --acls | |
| --xattrs | |
| --partial | |
| --info=progress2 | |
| --human-readable | |
| --checksum | |
| ) | |
| if $IS_DIR; then | |
| RSYNC_SRC="${SOURCE}/" | |
| RSYNC_DST="${DEST}/" | |
| else | |
| RSYNC_SRC="${SOURCE}" | |
| RSYNC_DST="${DEST}" | |
| fi | |
| $DRY_RUN && RSYNC_OPTS+=(--dry-run) | |
| log_step "开始同步..." | |
| if ! $SUDO rsync "${RSYNC_OPTS[@]}" "$RSYNC_SRC" "$RSYNC_DST"; then | |
| log_error "rsync 同步失败,源文件未被修改" | |
| exit 1 | |
| fi | |
| if $DRY_RUN; then | |
| log_step "[模拟] 校验完整性..." | |
| log_step "[模拟] 删除源文件: $SOURCE" | |
| log_step "[模拟] 创建软链接: $SOURCE -> $DEST" | |
| log_info "模拟运行完成,未做任何实际修改" | |
| exit 0 | |
| fi | |
| log_step "校验完整性..." | |
| VERIFY_OPTS=( | |
| --archive | |
| --checksum | |
| --dry-run | |
| --itemize-changes | |
| ) | |
| DIFF_OUTPUT=$($SUDO rsync "${VERIFY_OPTS[@]}" "$RSYNC_SRC" "$RSYNC_DST" 2>&1) | |
| if [[ -n "$DIFF_OUTPUT" ]]; then | |
| log_error "校验失败,源文件和目标文件存在差异:" | |
| echo "$DIFF_OUTPUT" | |
| log_error "源文件未被删除,请检查后重试" | |
| exit 1 | |
| fi | |
| log_info "校验通过" | |
| log_step "删除源文件: $SOURCE" | |
| if [[ -d "$SOURCE" ]]; then | |
| $SUDO chmod -R u+w "$SOURCE" 2>/dev/null || true | |
| fi | |
| if [[ -n "$TRASH_CMD" ]]; then | |
| $SUDO $TRASH_CMD "$SOURCE" | |
| else | |
| $SUDO rm -rf "$SOURCE" | |
| fi | |
| log_step "创建软链接: $SOURCE -> $DEST" | |
| $SUDO ln -s "$DEST" "$SOURCE" | |
| if [[ -L "$SOURCE" && "$(readlink -f "$SOURCE")" == "$(realpath "$DEST")" ]]; then | |
| log_info "完成! 已节省 $SOURCE_SIZE 固态硬盘空间" | |
| else | |
| log_error "软链接创建异常,请手动检查" | |
| exit 1 | |
| fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment