В этом сообщении показано, как использовать пакеты archive и compress для создания кода, который может программно создавать или извлекать сжатые файлы из архивных файлов в формате tar. Оба пакета используют идиому потокового ввода-вывода Go, которая упрощает чтение и запись данных из различных источников, которые можно сжимать и архивировать.

Исходный код этого сообщения https://github.com/vladimirvivien/go-tar

Деготь

Файл tar - это набор сегментов двоичных данных (обычно получаемых из файлов). Каждый сегмент начинается с заголовка, который содержит метаданные о двоичных данных, которые следуют за ними, и о том, как восстановить их в файл.

+---------------------------+
| [name][mode][uid][guild]  |
| ...                       |
+---------------------------+
| XXXXXXXXXXXXXXXXXXXXXXXXX |
| XXXXXXXXXXXXXXXXXXXXXXXXX |
| XXXXXXXXXXXXXXXXXXXXXXXXX |
+---------------------------+
| [name][mode][uid][guild]  |
| ...                       |
+---------------------------+
| XXXXXXXXXXXXXXXXXXXXXXXXX |
| XXXXXXXXXXXXXXXXXXXXXXXXX |
+---------------------------+

Пакет tar

Давайте начнем с простого примера, который использует данные в памяти (синтетические файлы) и помещает эти данные в архивный файл out.tar. Это показано, как работают разные части пакета tar.

В следующем разделе показано, как создавать файлы tar из реальных источников файлов.

Следующий фрагмент кода создает значение функции, присвоенное tarWrite, которое проходит через предоставленную карту (files) для создания сегментов tar для архива:

import(
    "archive/tar"
    ...
)
func main() {
   tarPath := "out.tar"
   files := map[string]string{
      "index.html": `<body>Hello!</body>`,
      "lang.json":  `[{"code":"eng","name":"English"}]`,
      "songs.txt":  `Claire de la lune, The Valkyrie, Swan Lake`,
   }
   tarWrite := func(data map[string]string) error {
      tarFile, err := os.Create(tarPath)
      if err != nil {
         log.Fatal(err)
      }
      defer tarFile.Close()
      tw := tar.NewWriter(tarFile)
      defer tw.Close()
      for name, content := range data {
         hdr := &tar.Header{
            Name: name,
            Mode: 0600,
            Size: int64(len(content)),
         }
         if err := tw.WriteHeader(hdr); err != nil {
            return err
         }
         if _, err := tw.Write([]byte(content)); err != nil {
            return err
         }
      }
      return nil
   }
   ...
   if err := tarWrite(files); err != nil {
     log.Fatal(err)
   }
}

Кислый файл https://github.com/vladimirvivien/go-tar/simple/tar1.go

В предыдущем фрагменте переменная tw создана как *tar.Writer, которая использует tarFile в качестве цели. Для каждого (синтетического) файла из карты data создается tar.Header, который определяет файл name, файл mode и файл size. Затем заголовок записывается с помощью tw.WriteHeader, за которым следует содержимое файла с помощью tw.Write.

Есть еще много других полей заголовка tar. Три проиллюстрированных минимума необходимы для создания функционального архива.

Когда код будет выполнен, он создаст файл out.tar. Проверить правильность создания архива можно с помощью команды tar -tvf:

Мы видим, что tar, как и ожидалось, содержит все три файла. Однако обратите внимание, что, поскольку мы использовали неполную информацию заголовка, некоторая информация о файле либо неверна, либо отсутствует (например, дата, право собственности на файл и т. Д.).

Чтобы проверить сгенерированный tar, используйте команду tar -xvf out.tar для извлечения файлов.

Программно файлы, содержащиеся в архиве, также могут быть извлечены с помощью пакета tar. Следующий фрагмент исходного кода открывает tar-файл и восстанавливает его содержимое на стандартном выводе:

func main() {
   tarPath := "out.tar"
   tarUnwrite := func() error {
      tarFile, err := os.Open(tarPath)
      if err != nil {
         return err
      }
      defer tarFile.Close()
      tr := tar.NewReader(tarFile)
      for {
         hdr, err := tr.Next()
         if err == io.EOF {
            break // End of archive
         }
         if err != nil {
            return err
         }
         fmt.Printf("Contents of %s: ", hdr.Name)
         if _, err := io.Copy(os.Stdout, tr); err != nil {
            return err
         }
         fmt.Println()
      }
      return nil
   }
   ...
   if err := tarUnWrite(files); err != nil {
      log.Fatal(err)
   }
}

В предыдущем фрагменте кода переменная tr типа *tar.Reader используется для извлечения файлов из архивного файла tarFile. Используя бесконечный цикл, код посещает каждый сегмент архива, чтобы восстановить его, распечатав стандартное содержимое. Первый шаг - получить заголовок раздела и убедиться, что файл не находится в EOF с помощью tr.Next(). Если не в EOF, тогда код считывает содержимое раздела (используя io.Copy) и печатает его.

Хотя эти примеры функционально завершены, они не являются лучшим способом использования пакета. В следующих разделах представлены несколько функций, которые добавляют более надежные нюансы для работы с файлами tar.

Tar из файлов

После прочтения предыдущего раздела читатели должны быть знакомы с элементами, необходимыми для создания архивов в кодировке tar и программного извлечения из них файлов. В этом разделе, однако, исследуется более распространенное использование создания файлов tar из файловых источников.

Функция tartar в следующем фрагменте кода создает файл tar из списка указанных paths. Он использует функции filetpath.Walk и filepath.WalkFunc (из пакета path) для обхода указанного дерева файлов:

import(
    "path/filepath"
)
func tartar(tarName string, paths []string) error {
   tarFile, err := os.Create(tarName)
   if err != nil {
      return err
   }
   defer tarFile.Close()
   tw := tar.NewWriter(tarFile)
   defer tw.Close()
   for _, path := range paths {
      walker := func(f string, fi os.FileInfo, err error) error {
         ...
         // fill in header info using func FileInfoHeader
         hdr, err := tar.FileInfoHeader(fi, fi.Name())
         ...
         // calculate relative file path
         relFilePath := file
         if filepath.IsAbs(path) {
            relFilePath, err = filepath.Rel(path, f)
            if err != nil {
               return err
            }
          }
          hdr.Name = relFilePath
          if err := tw.WriteHeader(hdr); err != nil {
             return err
          }
          // if path is a dir, go to next segment
          if fi.Mode().IsDir() {
             return nil
          }
          // add file to tar
          srcFile, err := os.Open(f) 
          ...
          defer srcFile.Close()
          _, err = io.Copy(tw, srcFile)
          if err != nil {
            return err
          }
          return nil
      }
      if err := filepath.Walk(path, walker); err != nil {
         fmt.Printf("failed to add %s to tar: %s\n", path, err)
      }
   }
   return nil
}

Полный источник github.com/vladimirvivien/go-tar/tartar/tartar.go

По большей части это соответствует тому же подходу, что и раньше, когда вся работа выполняется внутри функционального блока walker. Однако здесь вместо создания заголовка tar вручную используется функция tar.FileInfoHeader для правильного копирования os.FileInfo (из fi).

Обратите внимание, что при обнаружении каталога код просто записывает заголовок и переходит к следующему файлу без записи какого-либо содержимого. Это создает запись каталога в виде заголовка архива, что позволит сохранить точность древовидной структуры в файле tar.

Когда этот код создает tar, мы видим, что вся информация заголовка файла была добавлена ​​правильно и включает правильное время / дату, право собственности, режим файла и т. Д .:

Затем давайте посмотрим, как содержимое архива может быть извлечено в дерево файлов файловой системы программно. В следующем коде используется функция untartar для извлечения и восстановления файлов из tar-файла tarName по пути xpath:

func untartar(tarName, xpath string) (err error) {
   tarFile, err := os.Open(tarName)
   ...
   defer tarFile.Close()
   absPath, err := filepath.Abs(xpath)
   ...
   tr := tar.NewReader(tarFile)
 
   // untar each segment
   for {
      hdr, err := tr.Next()
      if err == io.EOF {
        break
      }
      if err != nil {
        return err
      }
      // determine proper file path info
      finfo := hdr.FileInfo()
      fileName := hdr.Name
      absFileName := filepath.Join(absPath, fileName)
      // if a dir, create it, then go to next segment
      if finfo.Mode().IsDir() {
         if err := os.MkdirAll(absFileName, 0755); err != nil {
           return err
         }
         continue
      }
      // create new file with original file mode
      file, err := os.OpenFile(
        absFileName, 
        os.O_RDWR|os.O_CREATE|os.O_TRUNC, 
        finfo.Mode().Perm(),
      )
      if err != nil {
        return err
      }
      fmt.Printf("x %s\n", absFileName)
      n, cpErr := io.Copy(file, tr)
      if closeErr := file.Close(); closeErr != nil {
        return err
      }
      if cpErr != nil {
        return cpErr
      }
      if n != finfo.Size() {
        return fmt.Errorf("wrote %d, want %d", n, finfo.Size())
      }
   }
   return nil
}

Опять же, механизм извлечения аналогичен тому, как это было сделано ранее. В цикле навсегда метод tr.Next используется для доступа к следующему заголовку в архивном файле. Если заголовок предназначен для каталога, код создает каталог и переходит к следующему заголовку.

Напомним, что в функции tartar header.Name обязательно должен быть относительным путем. Это гарантирует, что при извлечении файл будет помещен в соответствующий подкаталог.

Если заголовок предназначен для файла, файл создается с использованием os.OpenFile. Это гарантирует, что файл создается с правильным значением разрешения. Наконец, код использует функцию io.Copy для переноса содержимого из архива во вновь созданный файл.

Добавление сжатия

Пакет compress предлагает несколько форматов сжатия (включая gzip, bzip2, lzw и т. Д.), Которые можно легко включить в ваш код. Опять же, поскольку пакеты archive / tar и compress / gzip реализованы с использованием потоковых интерфейсов ввода-вывода Go, изменить код для сжатия содержимого файла архива с помощью gzip.

Следующий фрагмент обновляет функцию tartar для использования сжатия gzip, когда файл архива заканчивается на .gz:

import(
    "compress/gzip"
)
func tartar(tarName string, paths []string) (err error) {
   tarFile, err := os.Create(tarName)
   ...
   
   // enable compression if file ends in .gz
   tw := tar.NewWriter(tarFile)
   if strings.HasSuffix(tarName, ".gz"){
      gz := gzip.NewWriter(tarFile)
      defer gz.Close()
      tw = tar.NewWriter(gz)
   }
   defer tw.Close()
   ...
}

Предыдущее обновление кода - это все, что необходимо для сжатия содержимого, добавляемого в архив. Экземпляры io.Writer tw и gz связаны цепочкой с tarFile, что позволяет сжимать байты, предназначенные для tarFile, по мере их конвейерной передачи через gz. Довольно мило!

Файлы, сжатые с использованием tartar, можно проверить с помощью команды gzip:

> gzip -l tartar.tar.gz
  compressed uncompressed  ratio uncompressed_name
      724385      6213632  88.3% tartar.tar

Программно код может распаковывать закодированное содержимое tar при распаковке файлов из архива. Следующий фрагмент кода обновляет функцию untartar для цепочки io.Readers tarFile, gz и tr:

func untartar(tarName, xpath string) (err error) {
   tarFile, err := os.Open(tarName)
   ...
   tr := tar.NewReader(tarFile)
   if strings.HasSuffix(tarName, ".gz") {
      gz, err := gzip.NewReader(tarFile)
      if err != nil {
         return err
      }
      defer gz.Close()
      tr = tar.NewReader(gz)
   }
   ...
}

С этим изменением программа будет автоматически распаковывать содержимое заархивированных файлов с помощью gzip. Та же стратегия объединения может быть использована для поддержки других алгоритмов сжатия, реализующих API потокового ввода-вывода.

Заключение

Пакеты archive и compress в Go демонстрируют, как мощная стандартная библиотека может помочь программистам создавать серьезные инструменты. Оба пакета используют конструкции потокового ввода-вывода Go для работы со сжатыми файлами в кодировке tar. Проницательному или любопытному читателю рекомендуется обновить код, чтобы использовать другие алгоритмы архивирования или сжатия.

Также не забудьте проверить мою книгу о Go под названием Learning Go Programming от Packt Publishing.