В этом сообщении показано, как использовать пакеты 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.