解压一个 tar 包把家目录炸了:一次 Linux 归档打包排查复盘

直接解压一个日志 tar 包,几十个文件散落进家目录还覆盖了同名文件。排查梳理:tar 的 c/x/t/f 基本动作、z/j/J 压缩格式、tar bomb 与解压前先看/-C 指定目录、绝对路径 Removing leading / 的坑、排除与增量与权限与软链接,以及一套 tar 打包备份的保命纪律。

2024 年同事发来一个 logs.tar.gz,让我看看里面的报错日志。我图省事,直接在自己的家目录 ~/ 下敲了 tar -xzf logs.tar.gz。回车那一刻还没反应过来,几十个文件就"哗"地一下散落在我家目录里——access.logerror.logconfig.yml……和我自己的文件混作一团,我精心整理的目录瞬间一片狼藉,甚至有两个同名文件被直接覆盖。我盯着满屏的文件愣了半天:这个 tar 包里根本没有顶层目录,解压就是"原地爆炸"。这件事逼着我把 tar 这个天天用、却从来没真正搞懂的工具彻底理清了。本文复盘这次实战。

问题背景

环境:CentOS 7,日常用 tar 打包/解压
事故现象:
- 同事发来 logs.tar.gz,我在 ~/ 下直接解压
- 解压后几十个文件直接散落进家目录,没有任何顶层目录兜着
- 我自己的 config.yml 被压缩包里的同名文件【覆盖】了

现场排查:
# 1. 先别急着解压,看看这个包里【到底装了什么】
$ tar -tzf logs.tar.gz | head
access.log              # ★ 第一层就是文件,没有目录!
error.log
config.yml
app/run.log
...
# —— 这种包业界叫 "tar bomb"(tar 炸弹):
#    解压会把内容直接铺在当前目录,炸得到处都是

# 2. 对比一个"规范"的包长什么样
$ tar -tzf good.tar.gz | head
myproject/             # ★ 第一层是一个目录,所有东西在它下面
myproject/access.log
myproject/error.log
# —— 解压只会多出一个 myproject/ 目录,干净利落

根因(后来想清楚的):
1. tar 解压默认就在【当前目录】展开,不会自己建目录。
2. 那个包打包时,是在内容目录里 tar *,没带上层目录,
   于是包里第一层就是一堆裸文件 —— 解压必然铺一地。
3. 我解压前没有先 tar -t 看一眼内容,
   更没有用 -C 指定一个干净的解压目录。
说到底:不是 tar 的错,是我对 tar 的解压行为一无所知。

修复 1:tar 的基本形态——c / x / t / f

# === tar 的命令结构:一个动作 + 选项 + 文件 ===
# tar 的核心是四个互斥的"动作字母",一次只能用一个:
#   -c  create   创建一个新归档(打包)
#   -x  extract  从归档里解出文件(解压)
#   -t  list     只列出归档内容,不解压(★ 解压前必用)
#   -r  append   往已有归档里追加文件
# 还有一个几乎永远要带的:
#   -f  file     后面紧跟归档文件名(f 必须放选项最后)

# === 打包:把一个目录归档成一个 .tar 文件 ===
$ tar -cf myproject.tar myproject/
# -c 创建  -f 指定输出文件  最后是要打包的目录

# === 解压:把 .tar 解开 ===
$ tar -xf myproject.tar
# -x 解出  -f 指定要解的文件

# === ★ 解压前先列内容:养成肌肉记忆的一步 ===
$ tar -tf myproject.tar
# -t 只列出包里有什么,一个文件都不动。
# 这一步能让你提前发现"tar bomb"、看清目录结构。

# === 加 -v 看详细过程 ===
$ tar -cvf myproject.tar myproject/   # 打包时打印每个文件
$ tar -xvf myproject.tar              # 解压时打印每个文件
# -v 在排查"到底打进去/解出来了什么"时很有用。

# === 一个关键认知:c/x/t 三选一,f 后面接文件名 ===
# 记住 "f 永远紧贴文件名" —— 写成 -xf a.tar 没问题,
# 写成 -fx a.tar 也行,但别把 -f 和文件名拆开。

修复 2:压缩格式——z / j / J 与自动识别

# === tar 本身只"打包",不"压缩",压缩靠额外的选项 ===
# 纯 .tar 是把多个文件【拼】成一个,体积不会变小。
# 要变小,得叠加一个压缩算法:

# -z  gzip    .tar.gz / .tgz   速度快,压缩率中等(最常用)
# -j  bzip2   .tar.bz2         压缩率较高,速度较慢
# -J  xz      .tar.xz          压缩率最高,速度最慢

# === 打包并用 gzip 压缩(日常首选)===
$ tar -czf logs.tar.gz logs/
# c 创建 + z 走 gzip + f 指定文件名

# === 打包并用 xz 压缩(要极致小体积,比如发版)===
$ tar -cJf release.tar.xz release/

# === 解压:现代 tar 能【自动识别】压缩格式 ===
$ tar -xf logs.tar.gz       # 不写 -z,tar 自己认出来是 gzip
$ tar -xf release.tar.xz    # 不写 -J 也能解
# 新版 GNU tar 加了 -a/自动探测,解压时压缩字母可省。
# 但打包时必须显式写 -z/-j/-J,否则它只打包不压缩。

# === 三种格式实测对比(同一份 200MB 日志目录)===
# 格式        体积      打包耗时
# .tar       200 MB    几秒(没压缩)
# .tar.gz     38 MB    ~8 秒
# .tar.bz2    31 MB    ~25 秒
# .tar.xz     24 MB    ~50 秒
# —— 日常传输用 gz 够了;长期归档/分发包追求小,用 xz。

# === 想看压缩进度,配合 pv ===
$ tar -cf - logs/ | pv | gzip > logs.tar.gz
# tar -cf -  把归档写到标准输出,管道给 pv 看进度再 gzip。

修复 3:tar bomb——解压前先看,解压时指定目录

# === 这次的坑:"tar bomb" —— 包里第一层就是裸文件 ===
# 解压它,内容会【直接铺在当前目录】,和你已有的文件混在一起,
# 同名文件还会被悄无声息地覆盖。

# === 防御第一招:解压前永远先 -t 看一眼 ===
$ tar -tzf logs.tar.gz
access.log          # ← 第一层是文件 = 危险,是 tar bomb
error.log
# 对比:
$ tar -tzf good.tar.gz
myproject/          # ← 第一层是单个目录 = 安全
myproject/access.log

# === 防御第二招:用 -C 指定一个干净的解压目录 ===
$ mkdir /tmp/unpack
$ tar -xzf logs.tar.gz -C /tmp/unpack
# -C 让 tar 先 cd 到那个目录再解压。
# 哪怕这个包是 tar bomb,炸的也是 /tmp/unpack,
# 炸不到你的家目录。★ 解压陌生包,一律先建空目录 + -C。

# === 防御第三招:--one-top-level 自动套一层目录 ===
$ tar -xzf logs.tar.gz --one-top-level
# GNU tar 的这个选项:自动建一个和包同名的目录(logs/),
# 把所有内容塞进去 —— 等于强制给 tar bomb"兜底"。
$ tar -xzf logs.tar.gz --one-top-level=mydir   # 也可指定名字

# === 只解压包里的某个文件/目录 ===
$ tar -xzf logs.tar.gz error.log              # 只解出 error.log
$ tar -xzf logs.tar.gz app/                   # 只解出 app/ 这个目录
# 路径要和 tar -t 列出来的【完全一致】。

# === 打包时就别制造 tar bomb:带上顶层目录 ===
# 【错误】在内容目录里打包,包里全是裸文件:
$ cd logs/ && tar -czf ../logs.tar.gz *
# 【正确】在上层目录打包,把 logs 这个目录本身打进去:
$ tar -czf logs.tar.gz logs/
# 这样别人解压,只会规规矩矩多出一个 logs/ 目录。

修复 4:绝对路径的坑——Removing leading /

# === 用绝对路径打包,会埋下一个危险的雷 ===
$ tar -czf etc-backup.tar.gz /etc/nginx/
tar: Removing leading `/' from member names
# ★ 这行 warning 不是报错 —— 它在告诉你 tar 干了什么:
#   它把包里的路径从 /etc/nginx/... 改成了 etc/nginx/...
#   (去掉了开头的斜杠,变成了相对路径)

# === tar 为什么要去掉开头的斜杠 ===
# 设想包里存的是绝对路径 /etc/nginx/nginx.conf,
# 那不管你在哪里解压,它都会去写【真正的 /etc/nginx/】,
# 直接覆盖系统的线上配置 —— 这太危险了。
# 所以 tar 默认把绝对路径转成相对路径来"自保"。

# === 危险的写法:强行保留绝对路径 ===
$ tar -czf bad.tar.gz -P /etc/nginx/
# -P / --absolute-names:保留开头的斜杠。
# 这种包一旦在别的机器解压,会【直接覆盖那台机器的 /etc】。
# ★ 除非你非常清楚自己在做什么,否则永远别用 -P。

# === 正确姿势:用 -C 切到上层,再用相对路径打包 ===
$ tar -czf etc-nginx.tar.gz -C /etc nginx/
# -C /etc:先 cd 到 /etc   nginx/:相对它的相对路径
# 这样包里存的是 nginx/...,解压时落在【当前目录】下,
# 不会去碰系统的 /etc,安全可控。

# === 解压时也能强制"安全模式" ===
$ tar -xzf somepkg.tar.gz --no-same-owner --no-overwrite-dir
# 解压来历不明的包,可以加这类选项收紧行为。
# 更稳的还是修复 3 那招:建空目录 + -C 进去解。

# === 一句话总结这一节 ===
# 看到 "Removing leading /" 别忽略,它是 tar 在提醒你:
# "你用了绝对路径,我已经帮你转成相对路径了。"
# 正确做法是主动用 -C,而不是用 -P 去对抗它。

修复 5:排除、增量、权限与软链接

# === 排除不想打包的东西 ===
$ tar -czf src.tar.gz --exclude='*.log' --exclude='node_modules' src/
# --exclude 可以写多次,支持通配符。
# ★ 注意:--exclude 要写在【要打包的目录之前】才稳。
# 也可以把规则写进文件:
$ cat .tarignore
*.log
*.tmp
node_modules/
.git/
$ tar -czf src.tar.gz --exclude-from=.tarignore src/

# === 保留权限、属主(做系统级备份时重要)===
$ tar -czpf backup.tar.gz /data
# -p 保留权限位。解压时想完整还原属主属组:
$ tar -xzpf backup.tar.gz --same-owner
# 普通用户解压默认按自己的身份;root + --same-owner 才能还原属主。

# === 软链接:默认存"链接本身",-h 存"链接指向的内容" ===
$ tar -czf a.tar.gz mydir/          # 软链接原样打包成软链接
$ tar -czhf a.tar.gz mydir/         # -h 跟随软链接,把目标文件【实体】打进去
# 打包时如果链接的目标在包外,解压后就成了死链 ——
# 这种情况用 -h 把实体内容打进去更稳妥。

# === 增量备份:只打"变化的部分" ===
$ tar -czf full.tar.gz -g snapshot.snar /data       # 第一次:全量
$ tar -czf inc1.tar.gz -g snapshot.snar /data       # 之后:只打变化
# -g 指定一个"快照文件",tar 靠它记住上次备份的状态,
# 第二次只打包【新增/修改】的文件。适合每天增量备份。

# === 校验:打包后核对,解压前体检 ===
$ tar -czf data.tar.gz data/ && tar -tzf data.tar.gz >/dev/null && echo OK
# 打完包立刻 -t 过一遍,能解析成功才算这个包没坏。
$ tar -dzf data.tar.gz -C /原目录         # -d 把包和磁盘上的文件做 diff

# === 看包里有多少东西、占多大 ===
$ tar -tzvf data.tar.gz | wc -l           # 包里文件数
$ tar -tzvf data.tar.gz | awk '{s+=$3} END{print s}'   # 解压后总字节

修复 6:tar 打包备份的纪律

# === 这次事故暴露了用 tar 的随意,定几条纪律 ===

# === 1. ★ 解压任何包,先 tar -t 看,再决定怎么解 ===
$ tar -tzf 包.tar.gz | head
# 第一层是单个目录 -> 可以直接解
# 第一层是一堆裸文件 -> tar bomb,必须 -C 到空目录

# === 2. 解压陌生包,一律建空目录 + -C 进去 ===
$ mkdir /tmp/x && tar -xzf 包.tar.gz -C /tmp/x
# 永远别在家目录、/tmp 根、项目根这种地方直接解陌生包。

# === 3. 打包带上顶层目录,别在内容目录里 tar * ===
$ tar -czf logs.tar.gz logs/        # 对:别人解压只多一个 logs/
# 不要:cd logs && tar -czf ../x.tar.gz *

# === 4. 打包用相对路径,看到 "Removing leading /" 就改 ===
$ tar -czf x.tar.gz -C /上层 目标/   # 用 -C 切目录,而不是用 -P

# === 5. 一个稳妥的备份脚本骨架 ===
#!/bin/bash
set -euo pipefail
SRC=/data/www
DST=/backup
DATE=$(date +%F)
NAME="www-${DATE}.tar.gz"
# 打包(用 -C 切到上层,相对路径,排除垃圾)
tar -czpf "${DST}/${NAME}" \
    --exclude='*.log' --exclude='cache' \
    -C "$(dirname "$SRC")" "$(basename "$SRC")"
# ★ 打完立刻校验能否解析,坏包当场发现
tar -tzf "${DST}/${NAME}" >/dev/null || { echo "包损坏!"; exit 1; }
# 清理 7 天前的旧备份
find "$DST" -name 'www-*.tar.gz' -mtime +7 -delete
echo "备份完成: ${NAME}"

# === 6. 备份文件名带日期,别固定一个名一直覆盖 ===
# www-2024-05-14.tar.gz 这样,出事了还能回退到前几天的版本。

# === 7. 重要备份要校验、要异地 ===
$ sha256sum backup.tar.gz > backup.tar.gz.sha256   # 留校验和
# 备份只放本机 = 没有备份。务必再同步一份到别的机器。

命令速查

需求                        命令
=============================================================
打包(不压缩)               tar -cf  x.tar  目录/
打包 + gzip 压缩             tar -czf x.tar.gz  目录/
打包 + xz(体积最小)        tar -cJf x.tar.xz  目录/
★ 解压前先列内容            tar -tzf x.tar.gz
解压到指定目录              tar -xzf x.tar.gz -C /目标目录
自动套一层顶层目录          tar -xzf x.tar.gz --one-top-level
只解压包里某个文件          tar -xzf x.tar.gz 路径/文件
打包时排除                  tar -czf x.tar.gz --exclude='*.log' 目录/
用相对路径打包(避绝对路径) tar -czf x.tar.gz -C /上层 目标/
增量备份                    tar -czf x.tar.gz -g snap.snar 目录/
校验包能否正常解析          tar -tzf x.tar.gz >/dev/null

口诀:解压前必 tar -t 看一眼 -> 陌生包建空目录 + -C
      -> 打包带顶层目录 -> 看到 Removing leading 改用 -C

避坑清单

  1. tar 解压默认就在当前目录展开,不会自己建目录,陌生包必须先 -t 看内容
  2. 包里第一层是裸文件就是 tar bomb,解压会铺一地并覆盖同名文件
  3. 解压来历不明的包一律先 mkdir 空目录再 tar -xzf 包 -C 空目录
  4. --one-top-level 能强制给 tar bomb 套一层目录兜底
  5. tar 只打包不压缩,要变小得叠 -z(gz)/-j(bz2)/-J(xz)
  6. 打包必须显式写压缩字母,解压时现代 tar 能自动识别可省略
  7. 打包带上顶层目录,别 cd 进内容目录 tar *,否则做出 tar bomb 害别人
  8. 看到 Removing leading / 是 tar 在把绝对路径转相对,应主动用 -C 而不是 -P
  9. -P 保留绝对路径极危险,会让包在别的机器直接覆盖系统目录
  10. 备份文件名带日期别固定覆盖,打完包立刻 tar -t 校验,并异地再存一份

总结

这次解压一个日志包引发的小事故,把我对 tar 那种"不就是个打包解压工具吗"的轻慢,彻底纠正了过来。出事那一刻我特别懊恼,因为我什么命令都没敲错——tar -xzf logs.tar.gz,标准得不能再标准,可就是这一条标准命令,把几十个文件直接糊了我一脸,还覆盖掉了我自己的同名文件。事后我冷静下来复盘,才发现问题根本不在那条命令上,而在于我对 tar 解压这个动作的"行为模型"一无所知。我一直下意识地以为,解压一个压缩包,理所当然会得到一个和包同名的目录,所有内容都规规矩矩待在那个目录里——这其实是 Windows 上很多解压软件惯出来的错觉。但 tar 不是这样,tar 的解压行为朴素到了近乎"原始"的程度:它会把归档里记录的每一个路径,原原本本地、相对于你【当前所在的目录】铺开来。如果这个包在打包时,第一层就是一堆裸文件,那解压的结果就必然是这堆裸文件直接散落在你当前的目录里,和你已有的东西混在一起,撞了名字的就被无声地覆盖。这种第一层是裸文件、解压即"原地爆炸"的包,业界有个很形象的名字,叫 tar bomb,tar 炸弹。理解了这一点,我才真正想清楚,正确的解压姿势里,有两步是绝对不能省的。第一步,是在解压之前,永远先用 tar -t 把包里的内容列出来看一眼。这个动作几乎不花时间,一个文件都不会动,但它能让你在动手之前就看清楚:这个包的第一层,到底是一个规规矩矩兜住一切的顶层目录,还是一堆即将炸开的裸文件。第二步,是对任何来历不明的包,都先 mkdir 建一个干净的空目录,然后用 -C 选项让 tar 切到那个空目录里去解压。这样一来,哪怕这个包真的是个 tar bomb,它炸开的范围也被牢牢限制在那个我专门为它准备的空目录里,炸不到我的家目录,炸不到我的项目,炸不到任何我在乎的东西。这次排查也顺带让我读懂了另一个我过去一直忽略、甚至有点烦它的提示——打包时那行 Removing leading /。我以前总把它当成一个无关紧要的 warning 一扫而过,这次才明白,它其实是 tar 在郑重地向我交代它做了一件什么事:当我用绝对路径去打包时,tar 出于安全考虑,主动把归档里记录的路径从绝对路径改成了相对路径,因为如果它老老实实存绝对路径,那这个包不管在哪台机器上解压,都会去写那台机器真实的系统目录,直接覆盖掉线上的配置——这是灾难性的。所以 tar 默认帮我"拆弹",而我要做的,不是用 -P 选项去强行对抗它、把这颗雷重新装回去,而是顺着它的思路,主动用 -C 先切换到上层目录,再用相对路径来打包。从这次事故开始,我给自己立下了几条关于 tar 的铁律:解压任何一个包之前,必须先 tar -t 看一眼里面装的是什么;解压任何来历不明的包,必须先建一个空目录再用 -C 进去解;自己打包时,必须带上顶层目录,绝不在内容目录里 tar * 制造出 tar bomb 去坑别人;做备份时,文件名一定带上日期,打完包立刻用 tar -t 校验它没坏,并且再多存一份到别的机器上——只放在本机的备份,根本不算备份。这次从一地狼藉的家目录出发,我最大的收获,是终于把 tar 从一个"我以为闭着眼都会用"的简单工具,重新认识成了一个有着自己明确而朴素的行为逻辑的归档工具——而用好它的全部秘诀,就藏在那两个最容易被跳过的动作里:解压前,先看一眼;解压时,进空目录。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
Linux教程

rsync 一跑把对端文件搞乱了:一次 Linux 数据同步排查复盘

2026-5-20 17:57:19

Linux教程

find 清理脚本删错了别人的文件:一次 Linux 文件查找排查复盘

2026-5-20 18:03:32

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索