Tar档案保存硬链接

时间:2016-07-12 15:44:15

标签: go

在golang中使用archive / tar包,似乎无法访问文件所具有的硬链接数。但是,我记得在某个地方阅读tar目录或文件可以保留硬链接。

是否有一些包可以帮助我做到这一点?

2 个答案:

答案 0 :(得分:5)

tar确实保留了硬链接。

这是一个示例目录,其中包含三个硬链接文件和一个带有单个链接的文件:

foo% vdir .
total 16
-rw-r--r-- 3 kostix kostix 5 Jul 12 19:37 bar.txt
-rw-r--r-- 3 kostix kostix 5 Jul 12 19:37 foo.txt
-rw-r--r-- 3 kostix kostix 5 Jul 12 19:37 test.txt
-rw-r--r-- 1 kostix kostix 9 Jul 12 19:49 xyzzy.txt

现在我们使用GNU tar对其进行归档并验证它确实添加了链接 (因为我们没有传递--hard-dereferece命令行选项):

foo% tar -cf ../foo.tar .
foo% tar -tvf ../foo.tar
drwxr-xr-x kostix/kostix     0 2016-07-12 19:49 ./
-rw-r--r-- kostix/kostix     9 2016-07-12 19:49 ./xyzzy.txt
-rw-r--r-- kostix/kostix     5 2016-07-12 19:37 ./bar.txt
hrw-r--r-- kostix/kostix     0 2016-07-12 19:37 ./test.txt link to ./bar.txt
hrw-r--r-- kostix/kostix     0 2016-07-12 19:37 ./foo.txt link to ./bar.txt

archive/tar的文档是指在tar存档上定义标准的一堆文档(不幸的是,没有一个标准:例如,GNU tar不支持POSIX扩展属性,虽然BSD tar(依赖于libarchive),但pax)也是如此。 在硬链接上引用它:

  

LNKTYPE

     

此标志表示链接到任何类型的另一个文件的文件,   以前存档。这些文件在Unix中由每个文件识别   相同的设备和inode号码。链接名称在。中指定   linkname字段,尾随空格。

所以,一个hadrlink是一个特殊类型的enrty('1'),它引用了一些 之前(已经存档)文件的名称。

所以让我们创建一个游乐场示例。

我们对存档进行64位编码:

foo% base64 <../foo.tar | xclip -selection clipboard

...并写下the code。 存档包含一个目录,一个文件(类型'0')另一个文件(类型'0'),后跟两个硬链接(类型'1')。

游乐场示例的输出:

Archive entry '5': ./
Archive entry '0': ./xyzzy.txt
Archive entry '0': ./bar.txt
Archive entry '1': ./test.txt link to ./bar.txt
Archive entry '1': ./foo.txt link to ./bar.txt

所以你的链接计数代码应该:

  1. 逐个记录扫描整个档案。

  2. 记住任何常规文件(archive/tar.TypeReg类型 或者键入archive/tar.TypeRegA)已经处理过,并且有一个与之关联的计数器,从1开始。

    嗯,实际上,你最好是独家和记录条目 除了符号链接和目录之外的所有类型 - 因为tar 存档可以包含字符和块设备的节点,以及FIFO(命名管道)。

  3. 遇到硬链接(类型archive/tar.TypeReg)时,

    1. 阅读标题的Linkname字段。
    2. 查看“已查看”文件列表并增加计数器 与该名称相匹配的条目。
  4. 2016-07-13更新

    由于OP实际上想知道如何管理硬链接 源文件系统,这是更新。

    主要思想是在具有POSIX语义的文件系统上:

    • 指定文件的目录条目实际上指向特殊文件 文件系统元数据块称为“inode”。 inode包含目录条目的数量 指着它。

      创建硬链接实际上只是:

      1. 创建指向inode的新目录条目 原始(源)文件 - ln条款中的“链接目标”。
      2. 在该inode中增加链接计数器。
    • 因此,任何文件都由两个整数唯一标识: 标识托管文件系统的物理设备的“设备编号” 文件所在的位置,以及标识文件数据的inode编号。

      接下来,如果两个文件具有相同的(设备,inode)对, 它们代表相同的内容。或者,如果我们换一种说法,那就是一个 是另一方的硬链接。

    因此,将文件添加到tar存档,同时保留硬链接的方式是这样的:

    1. 添加文件后,将其(device,inode)对保存到某个查找表中。

    2. 添加其他文件时,找出其(设备,inode)对和 在那张桌子上查一查。

      如果找到匹配的条目,则文件的数据已经流式传输, 我们应该添加一个硬链接。

      否则,表现如步骤(1)。

    3. 所以这是代码:

      package main
      
      import (
          "archive/tar"
          "io"
          "log"
          "os"
          "path/filepath"
          "syscall"
      )
      
      type devino struct {
          Dev uint64
          Ino uint64
      }
      
      func main() {
          log.SetFlags(0)
      
          if len(os.Args) != 2 {
              log.Fatalf("Usage: %s DIR\n", os.Args[0])
          }
      
          seen := make(map[devino]string)
      
          tw := tar.NewWriter(os.Stdout)
      
          err := filepath.Walk(os.Args[1],
              func(fn string, fi os.FileInfo, we error) (err error) {
                  if we != nil {
                      log.Fatal("Error processing directory", we)
                  }
      
                  hdr, err := tar.FileInfoHeader(fi, "")
                  if err != nil {
                      return
                  }
      
                  if fi.IsDir() {
                      err = tw.WriteHeader(hdr)
                      return
                  }
      
                  st := fi.Sys().(*syscall.Stat_t)
                  di := devino{
                      Dev: st.Dev,
                      Ino: st.Ino,
                  }
      
                  orig, ok := seen[di]
                  if ok {
                      hdr.Typeflag = tar.TypeLink
                      hdr.Linkname = orig
                      hdr.Size = 0
      
                      err = tw.WriteHeader(hdr)
                      return
                  }
      
                  fd, err := os.Open(fn)
                  if err != nil {
                      return
                  }
                  err = tw.WriteHeader(hdr)
                  if err != nil {
                      return
                  }
                  _, err = io.Copy(tw, fd)
                  fd.Close() // Ignoring error for a file opened R/O
                  if err == nil {
                      seen[di] = fi.Name()
                  }
                  return err
              })
      
          if err != nil {
              log.Fatal(err)
          }
      
          err = tw.Close()
          if err != nil {
              log.Fatal(err)
          }
      
          return
      }
      

      请注意,它非常蹩脚:

      • 它不正确地处理文件和目录名称。

      • 它不会尝试正确使用符号链接和FIFO, 并跳过Unix域套接字等。

      • 它假定它在POSIX环境中有效。

        在非POSIX系统上,Sys()方法调用了类型的值 os.FileInfo可能会返回其他内容而不是POSIX'y syscall.Stat_t

        说,在Windows上,有多个不同的文件系统托管 “磁盘”或“驱动器”。我不知道Go如何处理。 对于这种情况,可能必须以某种方式模拟“设备编号”。

      另一方面,它显示了如何处理硬链接:

      • 设置标题结构的“链接名称”字段。
      • 将标题的“大小”字段重置为0(因为不会跟随数据)。

      您可能还想使用另一种方法来维护查找表:如果预期您的大多数文件位于同一物理文件系统上,则每个条目都会浪费uint64作为每个条目的设备编号。因此,地图层次结构可能是明智之举:第一个将设备编号映射到另一个地图,将地址编号映射到文件名。

      希望这有帮助。

答案 1 :(得分:0)

来自kostix的

答案很好而且很详细。只是要加上我的50美分,以了解如何在提取过程中保存这些硬链接。

在tar归档文件(tr.Next())中的所有文件上循环

if f.Size == 0 && f.Linkname != "" {
  // this is a hard link for another file. save it in map and create at the end
  seen[abs] = filepath.Join(extractDir, f.Linkname)
  continue
}

最后:

for path, target := range seen {
  if err := os.Link(target, path); err != nil {
    return fmt.Errorf("failed to create hard link: %v", err)
  }
}