取消删除被Mv覆盖的文件

2020-11-29 14:31:05

自从我们与您分享一个事件的故事以来已经有一段时间了,这可能是一件好事–过去一年中,我们所遇到的大多数运营事件本质上都“无聊”到可以轻松修复。这一次,我们已经一个纯粹的人为错误导致的数据丢失的故事,以及我们如何恢复数据的故事。

尽管这很令人尴尬,但我们仍然值得分享恢复的故事,因为它可能使您学到一些有用的信息,以防万一您遇到类似的情况。

如您所见,在过去的7个月中,我们已经将服务范围扩展到票务之外,只要全球大流行使传统活动的格式无法实现,我们的客户就可以将其活动转换为数字空间。我们努力的结果是建立了一家名为Venueless的合资企业,您应该绝对检查是否还没有。

我们在无场所上运行的虚拟事件的一个组成部分是实时视频流。在此过程中,我们的客户使用OBS或StreamYard之类的工具来创建实时视频流。然后通过RTMP将流发送到我们的编码服务器。在编码服务器上,我们将流重新编码为不同的质量级别,然后将其分发到我们自己的小型流CDN。

Venueless目前尚不包含视频点播组件,通常,我们的客户会在源头(例如使用OBS或StreamYard,并自行处理或发布。但是,为了安全起见,我们还会记录传入的流。目前,这不是我们促销服务的一部分,我们宁愿将其视为对客户的免费备份服务,以防客户录音。鉴于我们已经将其视为备份,因此我们目前不会对此数据进行任何进一步的备份。

通常,我们会在一段时间后删除这些录像,但是在某些情况下,我们的客户会要求我们获取这些录像,例如,因为他们自己的录像失败,或者是因为StreamYard仅记录了每个流的前8个小时。由于这种情况很少发生,因此在我们的系统中还不是自动化的过程。每当客户请求录制文件时,我们都会通过SSH进入相应的编码服务器,并将录制文件移至可通过HTTP访问的目录,如下所示:

就是这样,我们与客户共享链接,然后过程就完成了。这可能是最简单的步骤之一。昨天,一位客户要求我们提供他们活动的最后两个流的记录。就在本周结束之前,我想向他们提供所需的文件,并通过SSH进入服务器,寻找正确的文件并键入...

哎呀。我在键入public /之前先按回车键,然后用倒数第二个替换了最后一个流,丢失了其中一个视频。

我对文件系统的工作原理非常幼稚,因此我知道mv命令仅更改了文件系统的目录列表,但实际上并未从磁盘上擦除文件,因此我知道仍然有可能恢复文件,如果同时没有被其他文件覆盖。

由于我没有设法将根分区重新安装为只读以避免软损坏,因此我用大铁锤立即将所有只读分区重新安装:

嗯,好的,这行得通,但是现在如何安装任何数据恢复工具?经过一些实验,我认为最简单的方法是重新启动由我们的服务器提供商Hetzner提供的恢复系统。因此,我将引导加载程序配置为从网络引导其恢复系统,并强制重新引导服务器。

为了能够执行磁盘转储并具有一些操作灵活性,而无需将2 TB磁盘映像下载到我的本地计算机上(这可能需要一周的时间),我还快速购买了一个5 TB空间的Hetzner存储盒。

在执行致命的mv命令之前,我执行了ls -lisah以获取文件目录列表:

3146449 1.1G -rw-r--r-- 1 www-data www-data 1.1G 11月XX月XX:XX记录16678.flv3146113 1.6G -rw-r--r-- 1 www-data www-data 1.6 G 11月XX月XX:XX记录16679.flv

这意味着我知道已删除文件的索引节点号!如前所述,我对文件系统的理解是(而且现在)很幼稚,并且我非常乐观能够使用这些信息来恢复文件。这不是日记文件系统的用途吗?

以这种方式恢复文件似乎是不可能的。 ext4magicand extundelete是功能强大的工具,即使在尝试了两个小时以上的其他选项后,它们也确实在磁盘上找到了一些已删除的文件,但不是我一直在寻找的文件。

我没有花时间真正地了解ext4的工作原理,但是从我的各种博客中了解到,我的运气很差,因为inode不再包含相关信息,而ext4magic也无法从ext4中恢复必要的信息。日记。

debugfs:inode_dump 0000 a081 0000 8503 0000 e83a c15f e83a c15f .........:._。:._ 0020 e83a c15f 0000 0000 7200 0100 0800 0000。:._.... r .. ..... 0040 0000 0800 0100 0000 0af3 0100 0400 0000 ................ 0060 0000 0000 0000 0000 0100 0000 e6eb c000 ...... ..... 0100 0000 0000 0000 0000 0000 0000 0000 0000 ................ * 0140 0000 0000 92d0 2cf5 0000 0000 0000 0000 ......,... ............ 0160 0000 0000 0000 0000 0000 0000 6fb2 0000 ............ o ... 0200 2000 e3fb 208a 515b 7c65 5d5a 7c65 5d5a ... .Q [| e] Z | e] Z0220 e83a c15f 7c65 5d5a 0000 0000 0000 0000。:._ | e] Z ........ 0240 0000 0000 0000 0000 0000 0000 0000 0000 ............. ... *

但是,如果您遇到类似情况,则ext4magic操作方法确实很有帮助,值得一试。

互联网上通常建议使用另一种文件恢复方法,通常用于“小文本文件”:只需将整个磁盘复制到其中的已知部分即可!那么,为什么这也不适用于较大的非文本文件呢?

第一个问题显然是grep的目的。除了丢失的文件大小,我对丢失的文件唯一了解的是它是FLV视频文件。幸运的是,所有包含视频的FLV文件都以字节序列FLV \ x01 \ x05开始。因此,让我们在2 TB磁盘中搜索该字节序列,并打印出所有出现的字节偏移!

猫/ dev / md2 pv -s 1888127576000 \ | grep -P --byte-offset --text'FLV \ x01 \ x05' tee -a /mnt/storagebox/grep-log.txt

这大约花费了7个小时。带有(大约)磁盘总大小的pv命令是可选的,但它为您提供了一个不错的进度条。总体而言,这在我们的服务器上花费了6个多小时。

grep基于行工作,在二进制文件中表示“两个ASCII换行符之间的任何字节序列”。因此,日志文件包含许多这样的行:

搜索总共在我们的磁盘上找到126个FLV文件头。令人放心的是,由于文件系统仍然知道122个FLV文件,因此至少有四个没有文件名的FLV字节序列!

现在,我需要找出126个字节序列中的哪个没有文件名。由于我真的不想花整个周末来深入研究ext4磁盘布局,因此我寻求了一个更简单的解决方案:对于文件系统中仍然已知的每个文件,我都会计算文件前500 KB的哈希值:

#!/ usr / bin / python3 import glob import hashlib import os hashsize = 500 * 1024known_hashes = {} not_deleted_files = sorted(glob。glob('/mnt/disk/var/recordings/*.flv')+ glob。glob ('/mnt/disk/var/recordings/public/*.flv'))#忽略小于我们的哈希值的文件not_deleted_files = [如果在os中,则not_deleted_files中的f为f。统计(f)。 not_deleted_files中fname的st_size> hashsize]:打开(fname,'rb')为f:h = hashlib。 md5(f。read(hashsize))。 hexdigest()如果h在known_hashes中:print(“发现重复的哈希值:”)known_hashes [h] = fname print(h,fname)print(len(not_deleted_files),“带有”的文件,len(known_hashes),“哈希”)

有趣的是,来自完全不同的客户的两个文件共享前500 KB的相同哈希值,但我尚未对其进行测试,但我的理论是,这些文件流在刚开始的第一分钟内不包含任何音频或视频,而仅空框架。但是,由于我知道丢失的文件并非如此,因此我对继续使用此方法充满信心。

接下来,我为grep找到的每个字节计算了相同的哈希值,并将其与上一步中找到的哈希值进行了比较:

grep_log ='/mnt/storagebox/grep-log.txt'磁盘='/ dev / md2'打印(“ parsing grep log…”)position = []打开(grep_log,'rb')为f:用于输入行F 。阅读()。 split(b'\ n'):如果不是line:#忽略空行,例如在文件末尾继续pos,数据=行。 split(b':',1)pos = int(pos.decode())#在行binoffset = data中添加FLV的偏移量。索引(b“ FLV \ x01”)pos + = binoffset位置。 append(pos)print(“正在计算磁盘上的文件哈希...”)found_hashes = {},其中open(disk,'rb')为f:对于位置f中的p。求(p)d = f。读取(hashsize)h = hashlib。 md5(d)。 hexdigest()如果h在known_hashes中:print(“ At offset”,p,“找到已知哈希”,h,“对应”,known_hashes [h])else:print(“ At offset”,p,“发现未知哈希“,h)found_hashes [h] = p unknown_hashes = {h:p代表h,p在found_hashhes中。 items()如果h在known_hashes中不}} files_not_found = [f代表h,fname则在known_hashes中。 items()如果h不在found_hashes中,请打印(len(found_hashes),“ found散列”,len(unknown_hashes,“其中未知”))print(len(files_not_found),“找不到文件:”,files_not_found

这产生了带有校验和的5字节偏移量-正是我所期望的。四个确实不对应于文件的文件,一个对应于小于500 KB的文件,因此具有不同的哈希。

现在,剩下要做的就是从五个可能的字节偏移量开始写出(至少)1.6 GB的字节序列。为了安全起见,我分别导出了1.8 GB:

从数学导入层从tqdm从tqdm.auto导入tqdm (disk,'rb')as fr:fr。在open(f'out_ {h ..flv','wb')中以fw查找(p)作为fw:对于trange中的i(floor(size_missing_file // 1024)):fw。写(读(1024))

然后,我下载了五个文件,实际上,磁盘上位置最高的那个文件包含我不小心删除的视频文件。除了视频中不到一秒的轻微损坏之外,视频已完全恢复。 ew

从长远来看,我们当然会努力防止这种情况再次发生。除了别名mv ='mv -i'之外,还有很多特定的解决方案,如果客户开始比我们预期更多地依赖它,我们显然会重新评估是否需要为此数据创建单独的备份,并查看可能的视频-随着Venueless的点播功能的到来,我们将在某个时候创建​​一个全自动的视频处理管道,从而从该过程中删除手动和易于出错的步骤。