介绍
一套试听包应包含全部所需片段。如果某次转换失败,部分完成的目录不应看起来像已经交付的包。你将改进 Bash 脚本,先私下准备文件,遇错如实停止,只有全部转换成功才发布。
本实验有自己的录音、一个损坏的练习副本和一套无关的旧包,不使用上一实验的文件。请先完成“将一份试听扩展为批次”,或掌握其中的引号、FFmpeg 选项、函数、数组及原生退出状态。你还将使用 Bash 的 trap,只清理脚本自己拥有的临时目录。
让未完成的转换保持私有
本步骤中,你将让失败的转换不留下已发布批次,同时保持已有工作不变。
从支持 UTF-8 文件名的 Workspace 开始。如果相对路径不存在,用 cd 返回这里。
cd /home/labex/project/audio-studio
ls input library archive/previous
input 包含三份录音的练习副本。library 包含允许使用的完好原件。archive/previous 是必须保留的旧试听包,notes.txt 和 scratch-other 属于其他工作。只修改自己的脚本、练习副本和新的实验或输出目录。支持的输入是输入目录直接包含的非隐藏、扩展名为小写 .wav 的普通文件:22050 Hz、单声道、有符号 16 位 PCM,长度至少五秒。
用 cat 阅读初始脚本。即使转换失败,末尾的 exit 0 仍报告成功;过早执行的 mkdir 则让不完整的包暴露出来。
cat batch.sh
下面的试运行会有意遇到损坏录音。应看到以 Invalid data found when processing input 结尾的 FFmpeg 错误。这是文件格式失败,不是找不到命令或网络故障。
bash batch.sh input trial-pack 3
printf 'Starter exit: %s\n' "$?"
它误报 Starter exit: 0。ls trial-pack 只显示转换成功的文件;仅有一个目录不能证明完整。
ls trial-pack
只删除这个具名、可丢弃的试验目录。rm -r 递归删除目录及其内容,-- 表示选项结束。这条命令不会触碰旧包、原件或无关工作。
rm -r -- trial-pack
改进后的契约要求每次使用一个新的输出目录。已有文件、目录和符号链接——包括目标缺失的链接——都会被拒绝。输出的父目录必须已存在,输出本身必须位于输入之外。不支持并发写入;这里保护的是转换失败,不是崩溃或断电时的事务保证。
暂存目录在发布前容纳转换结果。mktemp -d 创建唯一命名的私有目录,替换 XXXXXX 后缀。它与最终输出同级,使最后移动发生在同一文件系统。trap '...' EXIT 会在脚本退出时清理。单引号将 $stage 的展开放到清理时才执行。rm -rf -- "$stage" 只删除那一个自有目录;不要改成 /tmp/* 或 .preview.* 这样的通配范围。
其他路径操作使契约更明确。$(command) 捕获文本;|| exit 2 在失败时停止。realpath -m 规范化尚不存在的目标,dirname 取得父目录。case "$output/" 的前缀测试拒绝位于输入内部的输出。给输入前缀加引号,可安全保留其字面字符,末尾的 * 才负责匹配后缀。mv -T 将暂存目录移动为准确的新目录名,而不是嵌入另一个目录。之前的存在性检查不是并发锁。
用完整改进版替换 batch.sh。数组、文件名提取、引号和 FFmpeg 参数与第一个引导实验相同。
cat > batch.sh <<'BASH'
#!/usr/bin/env bash
if [ "$#" -ne 3 ]; then printf 'Usage: batch.sh INPUT OUTPUT SECONDS\n' >&2; exit 2; fi
case "$3" in 1|2|3|4|5) seconds=$3 ;; *) printf 'Use 1 to 5 seconds\n' >&2; exit 2 ;; esac
if [ ! -d "$1" ]; then printf 'Input directory missing\n' >&2; exit 2; fi
if [ -e "$2" ] || [ -L "$2" ]; then printf 'Output already exists\n' >&2; exit 2; fi
input=$(realpath -- "$1") || exit 2
output=$(realpath -m -- "$2") || exit 2
parent=$(dirname -- "$output")
if [ ! -d "$parent" ]; then printf 'Output parent missing\n' >&2; exit 2; fi
case "$output/" in "$input/"*) printf 'Output must be outside input\n' >&2; exit 2 ;; esac
shopt -s nullglob
files=("$input"/*.wav)
if [ "${#files[@]}" -eq 0 ]; then printf 'No WAV inputs\n' >&2; exit 1; fi
stage=$(mktemp -d "$parent/.preview.XXXXXX") || exit 1
trap 'rm -rf -- "$stage"' EXIT
for source in "${files[@]}"; do
name=${source##*/}
if ! ffmpeg -nostdin -v error -i "$source" -t "$seconds" -c:a pcm_s16le "$stage/$name"; then
printf 'Could not convert: %s\n' "$name" >&2
exit 1
fi
done
if ! mv -T -- "$stage" "$output"; then exit 1; fi
printf 'Published %s previews\n' "${#files[@]}"
BASH
再次尝试同一组损坏练习输入。这次失败是有意的,先不要修复。
bash batch.sh input previews 3
printf 'Protected batch exit: %s\n' "$?"
应看到 Could not convert: zz Evening.wav 和 Protected batch exit: 1。显式失败分支停止了批次。previews 必须不存在,临时暂存目录也应已经清除。
ls -a 包括隐藏名称,因此可以用它寻找残留的 .preview. 暂存目录。它仍应显示 scratch-other,其中内容必须保持不变。
ls -a
运行本步骤检查。它会在一次性目录中测试你的原样脚本,不会修复练习输入或生成你的现场试听包。
恢复练习副本并发布
本步骤中,你将诊断失败录音,只恢复获准的练习副本,再用脚本发布完整包。
从 Workspace 开始。FFmpeg 可以不生成其他媒体文件就检查录音:-f null - 选择丢弃已解码采样的空输出。解码成功说明音频可读;文件损坏则返回非零。这里只对指定的课程练习副本进行检查。
ffmpeg -nostdin -v error -i "input/zz Evening.wav" -f null -
printf 'Decode exit: %s\n' "$?"
应看到格式错误和非零状态。library 中有同名完好文件。cp -- 将该原件复制到损坏练习副本上;两个带引号的路径准确指定文件。不要修改原始库本身。
cp -- "library/zz Evening.wav" "input/zz Evening.wav"
ffmpeg -nostdin -v error -i "input/zz Evening.wav" -f null -
printf 'Decode exit: %s\n' "$?"
现在应为零。如果文件名拼错,查看 ls library 并修正,无需重置环境。用恢复后的输入运行完整批次。
bash batch.sh input previews 3
应看到 Published 3 previews。切换到 Listening Room 并重新加载。在 Your listening pack 下逐一播放、定位,并下载一份。每个片段应为三秒,从对应录音的开头开始。页面只读取 previews;删除或损坏某份试听,不会让它悄悄播放库中原件。返回 Workspace。
最后,再尝试使用已经存在的输出。这次有意的拒绝会保护完整包。
bash batch.sh input previews 3
printf 'Existing output exit: %s\n' "$?"
应看到 Output already exists 和非零状态。保留该输出。以后制作另一套包时,选用新的名称、一至五秒的时长,并确保父目录已经存在。原始录音、旧包、笔记和无关目录都应保持不变。
总结
你分开了准备与发布,如实返回失败,并用 EXIT trap 清理一个自有暂存目录。你诊断并恢复了损坏的练习录音,然后在保留已有工作的同时发布完整、可播放的包。挑战将把相同工具应用于另一组试听素材。



