任务:一批视频文件,需要
- 自动化地转码成指定的格式
- 上传到服务器
- 得到文件的 URL 地址
对于转码工作,目前为止依然没有顺利的完成,由于网上资料贫乏,各种视频音频格式、编解码器、专利开源问题比较纠结,需要很长的时间理清这些关系。我重点研究了 FFmpeg 和 yamdi 这两个工具。但是今天用 yamdi 的时候发现一个很奇怪的 bug——它会自动改变原始视频文件的 fps 和 bitrate 作为输出文件,非常奇怪,可能会比较棘手。
对于第二个问题,经过几天的探索,顺带复习许久之前的 Bash Scripting 知识和众多的 Unix Power Tools,终于想出了比较完善可行的方案,诞生了人生第一个比较“成形”的 shell 脚本,惭愧……
首先给出我的脚本和模板配置文件,然后再逐步分析——
send_file.sh
:
#!/bin/bash
################################################################################
# Purpose: This interactive script is used for upload files onto a ftp server automatically.
# Author: Xiao Hanyu(xiaohanyu@taohua.com)
# Usage: ./send_file.sh
# Depends:
# lftp: used to transfer files
# tree: used to get the directory tree
# sed: used to get the filename
# sort: used to match the url and the filename into a csv file
# Notes:
# This script need a configuration files which is set by -f parameter.
# I have given a sample configuration file: sample.conf
# After running this script, it will output the [filename:url] list to a file
################################################################################
# This function checks whether the necessary tools are available
function usage
{
cat << EOF
`basename $0`: A utility to send files to a remote ftp server automatically
Usage: `basename $0` [Action]
Example: `basename $0` -f config_file
Actions:
-f: set the configuration files
-h: show this help
EOF
}
# check the necessary tools
function check_version
{
$1 --version > /dev/null
if [ $? -ne 0 ]
then
echo "You must install $1 before executing this script."
echo "You can type \"sudo apt-get install $1\" to finish this task under ubuntu os."
echo "exit ..."
exit 1
fi
}
# make sure that you have permission to create a temporary file under current directory
function check_perm
{
touch tmp_lftp_script_file
if [ $? -ne "0" ]
then
echo "You don't have permission to create temporary file under current directory."
echo "Type \"chmod u+w current_directory\" to give users write permissions."
echo "exit ..."
exit 1
else
rm -f tmp_lftp_script_file
fi
}
# this script use some file to store something, so
# if the file exists, we backup it into file.bak, and
# create a new empty file
function check_bak_file
{
if [ -e $1 ]
then
cp $1 $1.bak
fi
cat /dev/null > $1
}
# parse parameters
# -f for configuration file
# -h for command help
while getopts "f:h" arg
do
case $arg in
f)
config_file=$OPTARG
;;
h)
usage
exit 0
;;
?)
echo "!!Wrong command options!"
usage
exit 1
;;
esac
done
# check whether or not configuration file exists
if [ -e $config_file ]
then
# if $config_file exist, import the necessary variable
source $config_file
else
echo "configuration file not exist."
echo "exit ..."
exit 1
fi
# check lftp version
echo
check_version lftp
# check tree version
echo
check_version tree
# check permission
check_perm
# This file containts the command executed by lftp after login ftp server"
lftp_script=lftp_sh
check_bak_file $lftp_script
# $url_file store the [key:value] for filenames and urls
check_bak_file $url_file
# ftp anonymous login
username=${username:-"anonymous"}
password=${password:-"anonymous"}
# ftp default port
port=${port:-"21"}
# create lftp script executed by lftp
echo "lftp $username:$password@$host:$port" >> $lftp_script
echo "ls" >> $lftp_script
echo "cd $rdir" >> $lftp_script
for file in $lfiles
do
if [ -d $file ] # if $file is a directory, we should use 'lftp mirror -R' command
then
echo "mirror -R $file" >> $lftp_script
# use $(tree -ifp --noreport $file | grep "\[" | grep -v "\[d" | tr -s ' ' | cut -d' ' -f2 | sed -e 's/\.\{1,2\}\///g')
# to get all the filenames(contains relative path such "../../", "./", "../", "/", so we should use sed to get rid of these
for tmp_file in $(tree -ifp --noreport $file | grep "\[" | grep -v "\[d" | tr -s ' ' | cut -d' ' -f2 | sed -e 's/\.\{1,2\}\///g')
do
if [[ $rdir == "." || $rdir == "" ]] # if $rdir==".", we shouldn't give a url like 'http://hostname/./filename'
then
echo -e "$(basename $tmp_file | sed -e 's/\..*//g')\thttp://$host/$tmp_file" >> $url_file
else
echo -e "$(basename $tmp_file | sed -s 's/\..*//g')\thttp://$host/$rdir/$tmp_file" >> $url_file
fi
done
elif [ -e $file ]
then
echo "put $file" >> $lftp_script
if [[ $rdir == "." | $rdir == "" ]]
then
echo -e "$file\thttp://$host/$file" >> $url_file
else
echo -e "$file\thttp://$host/$rdir/$file" >> $url_file
fi
else
echo "!!Warning: $file not exist!"
fi
done
lftp -f $lftp_script
if [ $? -ne 0 ]
then
echo "Sending file failed, please check your ftp information."
echo "exit ..."
else
echo "Sending file successfully!"
fi
# echo "rm -f $lftp_script"
sample.conf
:
########################################
# Purpose: This file is the sample configuration file for the send_file utility
# Author: Xiao Hanyu(xiaohanyu@taohua.com)
# Warning:
# This file use bash script grammer to config, which means, you can't leave any space around '='
# Examples:
# a=b <<-->> right grammer
# b =c <<-->> wrong grammer
# c= d <<-->> wrong grammer
# d = f <<-->> wrong grammer
# Second, all the variables marked '!!' is necessary, others have default values
# Examples:
# config_file= #!!(necessary variable)
# username= (not necessary variable)
# Third, all the parameter should be quoted by ""
########################################
# username and password to login an ftp server
username="tiger"
password="tiger"
# hostname or ip of the remote ftp server
host="10.36.100.9" #!! necessary variable
# port, default is 21
port=
# local files, you should give the right absolute path
# or the right relative path
# both files are directories are allowed
# files and directories are seperated by [space] or [tab]
lfiles="send_file.sh sh_test sample.conf ../tmp t f g h ./sh_test"
# remote directory which you upload your files into
rdir="videos"
# specify the url_file
# url_file consists of two columns: filename and urls
url_file="url_list"
代码的注释比较详尽了,函数名称基本也能如实反映函数的作用,我来说明下基本思路。
首先是命令行选项的解析,这个根据复杂度不同有三种方法:
- 直接用
$1
、$2
、$3
手工处理,暴力解析。这里你需要知道几个 Bash 变量,如$0
代表 Bash 脚本的名字,$1
–$9
分别代表着第 1–9个命令行参数等等。优点是比较简单,缺点是太“简单” 了。 getopts
,Bash 内置,只支持短选项如-a -b -c
,-a option1 -b -c
,-abc
,不支持长选项如--version
这样的,使用比较简单(因为是 Bash 内置嘛)。getopt
,外部命令,比较复杂,支持长选项,我还不会用。
C++ Boost 库提供 Options 组件,用来解析命令行参数。具体的实例可以参见 Bash Shell 中命令行选项/参数处理。我的脚本中用的是第二种方法。
第二个大问题是参数选项的问题。我们可以通过两种方式配置参数,从而让我们的脚本自动化地做出适应性的处理。第一种方法是通过命令行参数,就是上面谈的 getopt
/getopts
,这种方法的好处就是方便直观快捷,变量解析可以用 Bash 内置的 read
或者高级一点的 TCL/Expect(这个我也不会),缺点在于每次敲命令的时候都要敲这一堆命令行参数,而且对于运维人员来说是一种非常不 user-friendly 的方式;第二种方法就是通过配置文件,让我们的 Bash 脚本自己解析指定的配置文件来获取相应的信息——比如 FTP 登录的 username 和 password、需要上传的文件、 上传的远端目录等等。
配置文件的格式有多种选择,Pluskid 大神的闲谈程序的配置文件是篇很不错的说明。我的脚本功能比较简单,配置文件自然也不会太复杂,因此我想出了一个非常“卑鄙无耻”的方法——就是直接将配置文件写成 bash script 变量赋值的形式,然后在脚本中通过这么一句:
直接引入配置变量。我承认我太卑鄙了,当然好处是简单可行——但是对于运维人员(使用这个脚本的人来说),可能会莫名奇妙——为啥等号后面不能有空格,为啥变量赋值最好要加引号——因为他们不懂 Shell Script 的语法——所以每次写脚本的时候、想象一下假设你就是那个要使用脚本的人,怎样才算友好的脚本?——但是我没有时间研究更复杂的脚本解析了——欢迎指正。
第三个大问题是 FTP 自动登录上传文件的问题。如果我们把平时的 FTP 登录操作比作用 Vim 编辑文件,那么自动化的 FTP 登录就是用 sed
来处理文件。想象一下,我们平时登录FTP,Windows 下,我们会点开一个 FTP 软件,点击快速链接,输入 username 和 password,然后下载上传。Linux 有万能的 LFTP 命令行工具,因此实现自动化的功能,从 LFTP 的参数选项着手是比较有希望的选择。
功夫不负有心人,LFTP 有两种手段能够实现自动化的登录上传下载。第一种方式是通过 lftp -f lftp_script_file
的方式, -f
指定一个文件 LFTP_script_file
,这个文件里面包含登录 LFTP 的命令和上传下载文件的命令。第二种方式是通过 LFTP 的 -u
参数指定登录名密码和 -e
选项指定登录后执行的 LFTP 命令。这种方式的缺点在于每执行一条命令都要登录一下 FTP——不过登录 FTP 所耗费的时间与上传文件的时间相比几乎可以忽略不计,所以也算不上一个大的缺点。
除了以上两种方式,我在扫 ABS 的时候偶然发现了 Here Documents 这个东西——这个曾经听说过但从来没有认真看过的东西,才发现这东西也有很多妙处,使用的当,同样可以实现 LFTP 的自动登录上传。我采用的是 LFTP 的 -f
选项,touch 一个临时文件完成自动登录上传的。
第四个问题是 URL 提取的问题。具体来说,比如你远端 FTP 和 HTTP 服务器的地址是 hostname
,远端目录是 videos
,本地上传文件是 send_file.sh
、 hpm.avi
,你需要生成如下的 [filename:url]
的 list:
send_file http://hostname/videos/send_file.sh
hpm http://hostname/videos/hpm.avi
然后存储这个 list 到一个文件里面,供后面进一步的 URL 生成映射处理之用。这个问题是耗时最久的一个问题。我的脚本里面有这么一段:
for file in $lfiles
do
if [ -d $file ] # if $file is a directory, we should use 'lftp mirror -R' command
then
echo "mirror -R $file" >> $lftp_script
# use $(tree -ifp --noreport $file grep "\[" | grep -v "\[d" | tr -s ' ' | cut -d' ' -f2 | sed -e 's/\.\{1,2\}\///g')
# to get all the filenames(contains relative path such "../../", "./", "../", "/", so we should use sed to get rid of these
for tmp_file in $(tree -ifp --noreport $file | grep "\[" | grep -v "\[d" | tr -s ' ' | cut -d' ' -f2 | sed -e 's/\.\{1,2\}\///g')
do
if [[ $rdir == "." || $rdir == "" ]] # if $rdir==".", we shouldn't give a url like 'http://hostname/./filename'
then
echo -e "$(basename $tmp_file | sed -e 's/\..*//g')\thttp://$host/$tmp_file" >> $url_file
else
echo -e "$(basename $tmp_file | sed -s 's/\..*//g')\thttp://$host/$rdir/$tmp_file" >> $url_file
fi
done
elif [ -e $file ]
then
echo "put $file" >> $lftp_script
if [[ $rdir == "." || $rdir == "" ]]
then
echo -e "$file\thttp://$host/$file" >> $url_file
else
echo -e "$file\thttp://$host/$rdir/$file" >> $url_file
fi
else
echo "!!Warning: $file not exist!"
fi
done
其中针对目录的处理尤为复杂,比如你可以指定 ../../tmp
这样的目录,如果你不作合适的处理,生成的 URL 可能是 http://hostname/../../tmp/
之类的东西。我最开始想的方法是递归目录的处理方法,但是写了好几个版本依然没有写出 Bash 的递归目录遍历。后来偶然间想到了 tree,这个可以列出目录树的命令,仔细研究了它的参数选项,同时以管道的方式结合其他命令如 grep(正向和反向匹配)、tr(压缩相同字符)、 cut(提取某个 column,可以用 awk 的 print 实现同样的功能)、sed(字符串处理,去除文件的路径和后缀),终于胜利地完成了这个任务。所谓成就感就是这么来的,哈。
至此,脚本需要解决的主要问题都已经阐述完毕,其余的问题都是一些小技俩,比如检查相关依赖工具是否安装、检查用户权限、提供帮助信息等等。目前发现一个 bug,还是目录上传的时候有时会出现递归上传的问题,非常奇怪。
脚本的改进之处也有很多,比如:
- 给出更加友好的提示帮助信息
- 给出更健壮的配置文件语法
- 自动检查每个文件是否上传成功,如果没有成功,能否实现断点续传
- 支持 log 文件输出,便于时候分析和故障分析
- 如果磁盘空间不够给出警告信息等等
OK,到此为止,睡觉去。