马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
背景与目标
从老系统/网盘恢复下来的照片视频,目录规则往往很乱(按应用、按批次、按来源)。我想把它们统一整理成新系统常见结构:
⚠️重要提醒
先做备份(或者至少先 dry-run + 小批量测试)。 “查重”这里只是 按文件大小判断,不是 hash/字节级校验(优点是快,缺点是可能误判)。 跨存储池/跨磁盘移动会变成“复制+删除”,速度会慢很多;同一存储池内 move 通常很快。 我用的是macos系统远程到ugos pro操作的,windows远程ssh的命令大家可以自行问AI
准备工作:明确目录与依赖
1)你需要确认的两个路径
DSTROOT="/home/DDD/Photos/MobileBackup/iPhone 15 Pro Max"SRC="/home/CAIT/restore/237555/我的相册"
2)工具依赖(新手最容易卡这一步)
脚本依赖这些命令:bash / awk / stat / mv / mkdir 以及 exiftool。 先检查 exiftool 是否存在: exiftool -ver如果提示找不到,需要先安装 exiftool(ugos pro里面是自带有这个工具的,印象中我没有特地安装)。
第 1 部分:创建脚本(生成清单 + 按清单移动)
0)SSH 登录并切 root ssh -p 22 用户名@192.168.xx.xx
输入密码
sudo -i
再次输入密码即可切换为root
脚本 1:生成清单脚本(plan)
作用:扫描来源目录 → 为每个文件计算“最早时间” → 输出两列 TSV:src<TAB>dst
cat >/root/make_plan_by_earliest_time.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
SRC="${1:?SRC missing}"
DSTROOT="${2:?DSTROOT missing}"
TS="$(date +%F_%H%M%S)"
PLAN="${3:-$DSTROOT/_plan_${TS}.tsv}"
mkdir -p "$DSTROOT"
exiftool -r -m -fast2 \
-ext heic -ext heif -ext jpg -ext jpeg -ext png -ext mov -ext mp4 -ext m4v \
-p '$FilePath|$DateTimeOriginal|$CreateDate|$MediaCreateDate|$FileModifyDate|$FileName' \
"$SRC" \
| awk -F'|' -v dst="$DSTROOT" '
BEGIN{OFS="\t"}
function norm(t, s){
if (t=="") return ""
s=t
sub(/[+-][0-9:]+$/,"",s); sub(/Z$/,"",s)
gsub(/[^0-9]/,"",s)
if (length(s) < 14) return ""
return substr(s,1,14)
}
function pick_earliest(a,b,c,d, i,t,min){
min=""
for (i=1; i<=4; i++){
t = (i==1?a:(i==2?b:(i==3?c:d)))
if (t!=""){
if (min=="" || t < min) min=t
}
}
return min
}
{
src=$1; dto=$2; cd=$3; mcd=$4; fmd=$5; name=$6
if (src=="" || name=="") next
if (name ~ /^\._/) next # AppleDouble
best = pick_earliest(norm(dto), norm(cd), norm(mcd), norm(fmd))
if (best=="") ym="0000/00"
else ym=substr(best,1,4)"/"substr(best,5,2)
print src, dst"/"ym"/"name
}
' > "$PLAN"
echo "PLAN=$PLAN"
EOF
chmod +x /root/make_plan_by_earliest_time.sh
bash -n /root/make_plan_by_earliest_time.sh && echo "make_plan syntax OK"
补充说明: plan 是 TSV(Tab 分隔),不是空格分隔。 取日期优先级:DateTimeOriginal / CreateDate / MediaCreateDate / FileModifyDate,并取这几个里最早的一个。 0000/00 通常代表:文件没有 EXIF、或者是下载/二次转存导致元数据缺失。
脚本 2:按清单移动脚本(带“按大小查重”和自动改名)
cat >/root/apply_plan_move_sizeprefix.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
DRYRUN=0
LIMIT=0
usage() {
echo "Usage: $0 [--dry-run] [--limit N] plan1.tsv [plan2.tsv ...]"
exit 1
}
PLANS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRYRUN=1; shift ;;
--limit) LIMIT="${2:-0}"; shift 2 ;;
-h|--help) usage ;;
*) PLANS+=("$1"); shift ;;
esac
done
[[ ${#PLANS[@]} -ge 1 ]] || usage
get_size() {
stat -c %s -- "$1" 2>/dev/null || echo ""
}
for PLAN in "${PLANS[@]}"; do
[[ -f "$PLAN" ]] || { echo "PLAN not found: $PLAN" >&2; exit 2; }
BASE="${PLAN%.tsv}"
LOG_MOV="${BASE}.move_ok.log"
LOG_SKP="${BASE}.skip_same_size.log"
LOG_RNM="${BASE}.renamed.log"
LOG_ERR="${BASE}.error.log"
RES_TSV="${BASE}.result.tsv"
: >"$LOG_MOV"; : >"$LOG_SKP"; : >"$LOG_RNM"; : >"$LOG_ERR"
echo -e "ACTION\tSRC\tDST_FINAL\tNOTE" >"$RES_TSV"
n=0; moved=0; skipped=0; renamed=0; err=0
while IFS=$'\t' read -r src dst; do
[[ -z "${src:-}" || -z "${dst:-}" ]] && continue
((n++))
# LIMIT:处理到 N 行就停(用于抽样/测试)
if [[ "$LIMIT" -gt 0 && "$n" -gt "$LIMIT"; then
echo "LIMIT reached: $LIMIT (stopped early) for $PLAN"
break
fi
if [[ ! -e "$src"; then
echo -e "ERROR\t$src\t$dst\tMISS_SRC" >>"$RES_TSV"
echo "MISS_SRC | $src -> $dst" >>"$LOG_ERR"
((err++))
continue
fi
mkdir -p "$(dirname -- "$dst")"
dst_final="$dst"
if [[ -e "$dst"; then
s_src="$(get_size "$src")"
s_dst="$(get_size "$dst")"
if [[ -n "$s_src" && -n "$s_dst" && "$s_src" == "$s_dst"; then
echo -e "SKIP\t$src\t$dst\tSAME_SIZE($s_src)" >>"$RES_TSV"
echo "SAME_SIZE($s_src) | $src -> $dst" >>"$LOG_SKP"
((skipped++))
continue
fi
# 同名但大小不同:前缀 1_,如仍冲突则 2_/3_...
dir="$(dirname -- "$dst")"
base="$(basename -- "$dst")"
i=1
while [[ -e "$dir/${i}_${base}"; do
((i++))
done
dst_final="$dir/${i}_${base}"
echo -e "RENAMED\t$src\t$dst_final\tCONFLICT_WITH:$dst" >>"$RES_TSV"
echo "CONFLICT | $src -> $dst_final (was $dst)" >>"$LOG_RNM"
((renamed++))
fi
if [[ "$DRYRUN"; then
echo "DRYRUN mv -- '$src' '$dst_final'"
echo -e "MOVED\t$src\t$dst_final\tDRYRUN" >>"$RES_TSV"
((moved++))
continue
fi
if mv -n -- "$src" "$dst_final" 2>>"$LOG_ERR"; then
echo -e "MOVED\t$src\t$dst_final\tOK" >>"$RES_TSV"
echo "OK | $src -> $dst_final" >>"$LOG_MOV"
((moved++))
else
echo -e "ERROR\t$src\t$dst_final\tMV_FAILED" >>"$RES_TSV"
echo "MV_FAILED | $src -> $dst_final" >>"$LOG_ERR"
((err++))
fi
done <"$PLAN"
echo "DONE($PLAN) total=$n moved=$moved renamed=$renamed skipped=$skipped err=$err"
echo "RESULT: $RES_TSV"
done
EOF
chmod +x /root/apply_plan_move_sizeprefix.sh
bash -n /root/apply_plan_move_sizeprefix.sh && echo "apply_plan syntax OK"
第 2 部分:生成清单(plan)
1)设定变量
DSTROOT="/home/DDD/Photos/MobileBackup/iPhone 15 Pro Max"SRC="/home/CAIT/restore/237555/我的相册" (路径改为实际路径)
2)生成 plan(文件多会跑一段时间)
/root/make_plan_by_earliest_time.sh "$SRC" "$DSTROOT" \ "$DSTROOT/_plan_album_$(date +%F_%H%M%S).tsv" (会在目标路径下生成)
第 3 部分:校验及移动
PLAN="刚才输出的PLAN路径"
1)必须两列(srcdst),bad_lines 必须=0 awk -F$'\t' 'NF!=2{bad++} END{print "bad_lines=",bad+0}' "$PLAN"
2)看前几行,确认是 Tab,且 dst 有 YYYY/MM 或 0000/00 head -n 5 "$PLAN" | sed -n 'l'
3)总行数(大致等于文件数) wc -l "$PLAN"
4)统计 0000/00(不应该离谱多) grep -c "/0000/00/" "$PLAN" 5)移动
/root/apply_plan_move_sizeprefix.sh "$PLAN" |& tee \
"$DSTROOT/_apply_$(date +%F_%H%M%S).console.log"
动作解释: 抛砖引玉,为大家提供一个思路,这只是我自己尝试过比较快捷可行的方案,如果大家有更好的工具和方法,欢迎分享。
如果中间出现什么不适配的,大家可以自行AI,上面的方法我已经验证过三个文件夹,还算成功,希望能帮到你。
|