使用linux时,经常会执行rm -rf命令,但是这一命令是有风险的,例如,执行某个shell脚本,shell脚本中有如下语句:
# script.sh
rm -rf $HOME/$SOME_PATH
此时,如果因为某项设置,导致环境变量SOME_PATH为空,则会直接把HOME目录下的所有内容清空。我在使用开发套件进行开发时,也出现过误删某一项目,导致本地和远程分支的代码一同被删除的问题,废了好大的劲才找到commit id恢复过来,和同事吐槽这一点的时候,同事表示自己做了个回收站功能,我一时兴起,也尝试做了个简单的回收站。
要实现一个类似windows回收站的功能,需要考虑以下问题:
- linux通常是多用户使用,这一脚本不应影响其他用户,即仅能在当前用户下使用,理想情况下,该脚本不能提升为root;
- 同一文件被多次删除时,应当能选择其中一项从回收站中进行恢复;
- 应当能查看被删除文件的meta信息,例如,size,被删日期,原路径,etc;
- 能正确处理文件/目录/软链接;
- 能识别正则匹配的参数,例如rm -rf test*,能匹配出test1,test2,etc
这个功能并不难实现。
针对第一点,将该用户脚本限定在HOME目录下即可;
针对第二点,可通过绝对路径+被删日期生成的hash值/md5来唯一标识某个文件,并提供恢复文件的脚本;
针对第三点,可提供一个文件,专门用于记录被删除的文件的必要信息;
第四点无需操心,linux下一切皆文件;第五点实测发现,shell脚本会对正则匹配的参数展开为实际值。
该功能主要分为两个部分,分别为remove.sh和recover.sh,其共同操作一个recycle目录以及一个meta文件,对于两者公用的变量,放在init.conf中。
将文件移到回收站
#!/bin/bash
# remove.sh
source ./init.conf
# init
if [ ! -d ${TRASH} ]
then
mkdir ${TRASH}
echo “recycle ${TRASH} created”
fi
if [ ! -f ${TRASH_META} ]
then
touch ${TRASH_META}
echo “file ${TRASH_META} created”
fi
# do remove
for f in $*
do
if [ ! -e ${f} ]
then
echo -e "\e[31m WARN \e[0m: ${f} not exists"
else
real_path=$(realpath ${f})
if [ ${real_path} = ${TRASH} -o ${real_path} = ${TRASH_META} ]
then
echo -e "\e[31m WARN \e[0m: ${f} not exists"
else
cur_time=$(date +%G-%m-%dT%T)
unique_file=${real_path}+${cur_time}
encode_file=$(echo -n ${unique_file} | md5sum | cut -d ' ' -f1)
# write to meta file
echo "[FILE NAME]:${real_path}; [DELETE TIME]:${cur_time}; [MD5]:${encode_file}" >> ${TRASH_META}
# mv the deleted file to recycle
mv $f ${TRASH}/${encode_file}
fi
fi
done
remove.sh接受的参数形式与rm命令相同。
shell脚本中的
\e[31m xxx \e[0m
是一个小trick,用于在命令行中输出不同的颜色以作区分,上述的31m表示红色。
变量${TRASH}和${TRASH_META}定义在init.conf中,为避免回收站目录被误删,我们将其设置为隐藏目录。回收站的上级目录最好设置为一个硬盘较大且有权限的目录,简单起见,这里设置为$HOME:
# hidden dir/file, to avoid being deleted unconsciously
TRASH=${HOME}/.recycle
TRASH_META=${HOME}/.meta
整个脚本简单明了,唯一需要考虑的是“自删除”问题,即禁止该脚本删除recycle目录。
下面是recover.sh
将文件从回收站中恢复
#!/bin/bash
# recover.sh
source ./init.conf
for md5 in $*
do
recover_file=$(cat ${TRASH_META} | grep ${md5} | cut -d ';' -f1 | cut -d ':' -f2)
if [ -z ${recover_file} ]
then
echo -e "\e[31m WARN \e[0m: can not locate recover file, perhaps the md5(${md5}) you input is invalid"
else
if [ ! -e ${TRASH}/${md5} ]
then
echo -e "\e[31m WARN \e[0m: ${TRASH}/${md5} not exists!"
else
mv ${TRASH}/${md5} ${recover_file}
if [ $? != 0 ]
then
dir_path=$(echo -n ${recover_file} | rev | cut -d '/' -f 2- | rev)
mkdir -p ${dir_path}
if [ $? != 0 ]
then
echo "failed to create directory ${dir_path}"
else
# parent dies have been created, try mv again
mv ${TRASH}/${md5} ${recover_file}
fi
fi
# the file has been recovered, remove the specific line from meta file
sed -ie "/${md5}/d" ${TRASH_META}
fi
fi
done
recover.sh接受若干个参数,每个参数均为一个md5值,若该md5值不存在,或者md5值对应的原文件不存在,则会报错。由于md5至多出现一次,因此我们直接通过grep+sed的组合命令定位到原始文件的绝对路径。
为什么是传入的参数是md5呢?
在windows中,我们若要恢复某个被删的文件,需要打开回收站,选择某个文件,再点击恢复操作;对应这里的回收站,则是打开.meta文件,查看要恢复的文件,然后选中其对应的md5值,再作为参数传入recover.sh中进行恢复。
恢复的过程中我们加了一个额外的检测:假设我们要恢复${HOME}/test/1这个文件,但是test目录已经被删除,此时执行mv命令是会报错的,正确做法是如果发现目录不存在,则通过mkdir -p命令递归创建后再执行mv命令。这里假定mv命令出错的原因是目录不存在,其实是不够robust的,对于其它错误(比如硬盘空间不足),并没有进行处理。
其它操作
初始化脚本执行环境
我们希望用户能在任意地方都能执行这一命令,因此可以考虑将其加入到用户的环境变量中。假定remove.sh, recover.sh, init.conf都在remove目录下,那么在该目录下新建一个export.sh,以初始化环境变量:
#!/bin/bash
# export.sh
remove_root=$(pwd)
find_remove=(echo ${PATH} | grep ${remove_root})
if [ ${find_remove} ]
then
echo "export PATH=${remove_root}:${PATH}" >> ${HOME}/.bashrc
source ${HOME}/.bashrc
fi
定时清空回收站
随着时间不断推移,回收站占据的空间势必会越来越大。从需求来看,我们最初执行rm命令,就是为了永久删除某个文件,回收站只是为了恢复极少数出现的误删的文件,一个庞大而臃肿的回收站并不是我们愿意看到的。因此,可以考虑增加一项定时任务,来定期清理回收站。例如,自动清理回收站中超过7天的文件。
#!/bin/bash
# clean_recycle.sh
source ./init.conf
seven_days_before=$(date +%G-%m-%dT-%T --date='7 day')
sdb_ts=$(date -d ${seven_days_before} +%s)
if [ ! -f ${TRASH_META} ]
then
echo "\e[31m WARN \e[0m: meta file [${TRASH_META}] not exists!"
exit
else
while read LINE
do
delete_time=$(echo -n ${LINE} | cut -d ';' -f 2 | cut -d ':' -f1)
md5=$(echo -n ${LINE} | cut -d ';' -f 3 | cut -d ':' -f1)
if [ -z ${delete_time} ]
then
echo "invalid time"
else
delete_ts=$(date -d ${delete_time} +%s)
fi
if [ ${delete_ts} < ${sdb_ts}]
then
if [ ! -e ${TRASH}/${md5} ]
then
echo "md5[${md5}] file not exists!"
else
# do real remove
rm -rf ${TRASH}/${md5}
sed -ie "/${md5}/d" ${TRASH_META}
fi
fi
done < ${TRASH_META}
fi
clean_recycle.sh是在脚本里写死了清空7天前的文件,如果想调整的话,还要改脚本,其实显得不是很灵活,更好的方式是按参数传递,或者写在init.conf中,这里图省事,就没考虑那么多了。
脚本中的rm命令是有风险的:如果${TRASH}变量被其它程序清空,那么rm命令就会把根目录一同删除,因此在rm前必须要检测被删除的md5文件是否存在。
这个脚本可以以定时任务或者后台任务的方式存在,考虑到实际开发环境中,后台进程经常会因为各种原因被kill掉,因此以定时任务的方式,每天凌晨运行一遍:
chmod +x clean_recycle.sh
crontab -l
59 23 * * * ${HOME}/remove/clean_recycle.sh
最后,remove.sh也是有可能会被rm命令给删除的,一个比较trick的方式是用chattr命令使其只读:
chattr +i remove.sh recover.sh clean_recycle.sh
改为只读模式后,即便sudo rm也无法删除这些文件。
至此,一个简单的linux回收站功能实现完毕。
github地址见linux_recycle