admin管理员组

文章数量:1567556

使用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

本文标签: 回收站类似功能LinuxShell