Rework of archiver

Common approach for working with archives is introduced.
Unpacker and Archiver interfaces are added.

Example of file unpacking:
 archive, _ := debos.NewArchive("file.ext")
 archive.Unpack("destination")

Three archive types are supported:
- tar
- zip
- debian packages

Type of archive is guessed by extension or can be set explicitly during
archiver creation with option for NewArchive().

Signed-off-by: Denis Pynkin <denis.pynkin@collabora.com>
This commit is contained in:
Denis Pynkin
2017-09-25 20:58:31 +03:00
parent f203f19654
commit 061e0eb6ac

View File

@@ -1,33 +1,217 @@
package debos
import (
"log"
"fmt"
"os"
"path/filepath"
"strings"
)
func TarOptions(compression string) string {
type ArchiveType int
// Supported types
const (
_ ArchiveType = iota // Guess archive type from file extension
Tar
Zip
Deb
)
type ArchiveBase struct {
file string // Path to archive file
atype ArchiveType
options map[interface{}]interface{} // Archiver-depending map with additional hints
}
type ArchiveTar struct {
ArchiveBase
}
type ArchiveZip struct {
ArchiveBase
}
type ArchiveDeb struct {
ArchiveBase
}
type Unpacker interface {
Unpack(destination string) error
RelaxedUnpack(destination string) error
}
type Archiver interface {
Type() ArchiveType
AddOption(key, value interface{}) error
Unpacker
}
type Archive struct {
Archiver
}
// Unpack archive as is
func (arc *ArchiveBase) Unpack(destination string) error {
return fmt.Errorf("Unpack is not supported for '%s'", arc.file)
}
/*
RelaxedUnpack unpack archive in relaxed mode allowing to ignore or
avoid minor issues with unpacker tool or framework.
*/
func (arc *ArchiveBase) RelaxedUnpack(destination string) error {
return arc.Unpack(destination)
}
func (arc *ArchiveBase) AddOption(key, value interface{}) error {
if arc.options == nil {
arc.options = make(map[interface{}]interface{})
}
arc.options[key] = value
return nil
}
func (arc *ArchiveBase) Type() ArchiveType { return arc.atype }
// Helper function for unpacking with external tool
func unpack(command []string, destination string) error {
if err := os.MkdirAll(destination, 0755); err != nil {
return err
}
return Command{}.Run("unpack", command...)
}
// Helper function for checking allowed compression types
// Returns empty string for unknown
func tarOptions(compression string) string {
unpackTarOpts := map[string]string{
"gz": "-z",
"bzip2": "-j",
"xz": "-J",
} // Trying to guess all other supported formats
} // Trying to guess all other supported compression types
return unpackTarOpts[compression]
}
func UnpackTarArchive(infile, destination, compression string, options ...string) error {
if err := os.MkdirAll(destination, 0755); err != nil {
return err
}
log.Printf("Unpacking %s\n", infile)
func (tar *ArchiveTar) Unpack(destination string) error {
command := []string{"tar"}
command = append(command, options...)
command = append(command, "-x")
if unpackTarOpt := TarOptions(compression); len(unpackTarOpt) > 0 {
command = append(command, unpackTarOpt)
if options, ok := tar.options["taroptions"].([]string); ok {
for _, option := range options {
command = append(command, option)
}
}
command = append(command, "-f", infile, "-C", destination)
command = append(command, "-C", destination)
command = append(command, "-x")
return Command{}.Run("unpack", command...)
if compression, ok := tar.options["tarcompression"]; ok {
if unpackTarOpt := tarOptions(compression.(string)); len(unpackTarOpt) > 0 {
command = append(command, unpackTarOpt)
}
}
command = append(command, "-f", tar.file)
return unpack(command, destination)
}
func (tar *ArchiveTar) RelaxedUnpack(destination string) error {
taroptions := []string{"--no-same-owner", "--no-same-permissions"}
options, ok := tar.options["taroptions"].([]string)
defer func() { tar.options["taroptions"] = options }()
if ok {
for _, option := range options {
taroptions = append(taroptions, option)
}
}
tar.options["taroptions"] = taroptions
return tar.Unpack(destination)
}
func (tar *ArchiveTar) AddOption(key, value interface{}) error {
switch key {
case "taroptions":
// expect a slice
options, ok := value.([]string)
if !ok {
return fmt.Errorf("Wrong type for value")
}
tar.options["taroptions"] = options
case "tarcompression":
compression, ok := value.(string)
if !ok {
return fmt.Errorf("Wrong type for value")
}
option := tarOptions(compression)
if len(option) == 0 {
return fmt.Errorf("Compression '%s' is not supported", compression)
}
tar.options["tarcompression"] = compression
default:
return fmt.Errorf("Option '%v' is not supported for tar archive type", key)
}
return nil
}
func (zip *ArchiveZip) Unpack(destination string) error {
command := []string{"unzip", zip.file, "-d", destination}
return unpack(command, destination)
}
func (zip *ArchiveZip) RelaxedUnpack(destination string) error {
return zip.Unpack(destination)
}
func (deb *ArchiveDeb) Unpack(destination string) error {
command := []string{"dpkg", "-x", deb.file, destination}
return unpack(command, destination)
}
func (deb *ArchiveDeb) RelaxedUnpack(destination string) error {
return deb.Unpack(destination)
}
/*
NewArchive associate correct structure and methods according to
archive type. If ArchiveType is omitted -- trying to guess the type.
Return ArchiveType or nil in case of error.
*/
func NewArchive(file string, arcType ...ArchiveType) (Archive, error) {
var archive Archive
var atype ArchiveType
if len(arcType) == 0 {
ext := filepath.Ext(file)
ext = strings.ToLower(ext)
switch ext {
case ".deb":
atype = Deb
case ".zip":
atype = Zip
default:
//FIXME: guess Tar maybe?
atype = Tar
}
} else {
atype = arcType[0]
}
common := ArchiveBase{}
common.file = file
common.atype = atype
common.options = make(map[interface{}]interface{})
switch atype {
case Tar:
archive = Archive{&ArchiveTar{ArchiveBase: common}}
case Zip:
archive = Archive{&ArchiveZip{ArchiveBase: common}}
case Deb:
archive = Archive{&ArchiveDeb{ArchiveBase: common}}
default:
return archive, fmt.Errorf("Unsupported archive '%s'", file)
}
return archive, nil
}