blob: 57f9ddeecaa579cefb79bac05a9023e778c58d66 [file] [log] [blame]
// Package packages provides functionality to install ZIP and TAR packages.
package packages
import (
"archive/tar"
"archive/zip"
"compress/bzip2"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"veyron.io/veyron/veyron2/services/mgmt/repository"
)
const defaultType = "application/octet-stream"
var typemap = map[string]repository.MediaInfo{
".zip": repository.MediaInfo{Type: "application/zip"},
".tar": repository.MediaInfo{Type: "application/x-tar"},
".tgz": repository.MediaInfo{Type: "application/x-tar", Encoding: "gzip"},
".tar.gz": repository.MediaInfo{Type: "application/x-tar", Encoding: "gzip"},
".tbz2": repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"},
".tb2": repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"},
".tbz": repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"},
".tar.bz2": repository.MediaInfo{Type: "application/x-tar", Encoding: "bzip2"},
}
// MediaInfoForFileName returns the MediaInfo based on the file's extension.
func MediaInfoForFileName(fileName string) repository.MediaInfo {
fileName = strings.ToLower(fileName)
for k, v := range typemap {
if strings.HasSuffix(fileName, k) {
return v
}
}
return repository.MediaInfo{Type: defaultType}
}
// Install installs a package in the given directory. If the package is a TAR or
// ZIP archive, its content is extracted in the destination directory.
// Otherwise, the package file itself is copied to the destination directory.
func Install(pkgFile, dir string) error {
mediaInfo, err := LoadMediaInfo(pkgFile)
if err != nil {
return err
}
switch mediaInfo.Type {
case "application/x-tar":
return extractTar(pkgFile, mediaInfo, dir)
case "application/zip":
return extractZip(pkgFile, dir)
default:
return fmt.Errorf("unsupported media type: %v", mediaInfo.Type)
}
}
// LoadMediaInfo returns the MediaInfo for the given package file.
func LoadMediaInfo(pkgFile string) (repository.MediaInfo, error) {
jInfo, err := ioutil.ReadFile(pkgFile + ".__info")
if err != nil {
return repository.MediaInfo{}, err
}
var info repository.MediaInfo
if err := json.Unmarshal(jInfo, &info); err != nil {
return repository.MediaInfo{}, err
}
return info, nil
}
// SaveMediaInfo saves the media info for a package.
func SaveMediaInfo(pkgFile string, mediaInfo repository.MediaInfo) error {
jInfo, err := json.Marshal(mediaInfo)
if err != nil {
return err
}
infoFile := pkgFile + ".__info"
if err := ioutil.WriteFile(infoFile, jInfo, os.FileMode(0600)); err != nil {
return err
}
return nil
}
func extractZip(zipFile, installDir string) error {
zr, err := zip.OpenReader(zipFile)
if err != nil {
return err
}
for _, file := range zr.File {
fi := file.FileInfo()
name := filepath.Join(installDir, file.Name)
if !strings.HasPrefix(name, installDir) {
return fmt.Errorf("failed to extract file %q outside of install directory", file.Name)
}
if fi.IsDir() {
if err := os.MkdirAll(name, os.FileMode(fi.Mode()&0700)); err != nil && !os.IsExist(err) {
return err
}
continue
}
in, err := file.Open()
if err != nil {
return err
}
parentName := filepath.Dir(name)
if err := os.MkdirAll(parentName, os.FileMode(0700)); err != nil {
return err
}
out, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY, os.FileMode(fi.Mode()&0700))
if err != nil {
in.Close()
return err
}
nbytes, err := io.Copy(out, in)
in.Close()
out.Close()
if err != nil {
return err
}
if nbytes != fi.Size() {
return fmt.Errorf("file size doesn't match for %q: %d != %d", fi.Name(), nbytes, fi.Size())
}
}
return nil
}
func extractTar(pkgFile string, mediaInfo repository.MediaInfo, installDir string) error {
f, err := os.Open(pkgFile)
if err != nil {
return err
}
defer f.Close()
var reader io.Reader
switch enc := mediaInfo.Encoding; enc {
case "":
reader = f
case "gzip":
var err error
if reader, err = gzip.NewReader(f); err != nil {
return err
}
case "bzip2":
reader = bzip2.NewReader(f)
default:
return fmt.Errorf("unsupported encoding: %q", enc)
}
tr := tar.NewReader(reader)
for {
hdr, err := tr.Next()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
name := filepath.Join(installDir, hdr.Name)
if !strings.HasPrefix(name, installDir) {
return fmt.Errorf("failed to extract file %q outside of install directory", hdr.Name)
}
// Regular file
if hdr.Typeflag == tar.TypeReg {
out, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY, os.FileMode(hdr.Mode&0700))
if err != nil {
return err
}
nbytes, err := io.Copy(out, tr)
out.Close()
if err != nil {
return err
}
if nbytes != hdr.Size {
return fmt.Errorf("file size doesn't match for %q: %d != %d", hdr.Name, nbytes, hdr.Size)
}
continue
}
// Directory
if hdr.Typeflag == tar.TypeDir {
if err := os.Mkdir(name, os.FileMode(hdr.Mode&0700)); err != nil && !os.IsExist(err) {
return err
}
continue
}
// Skip unsupported types
// TODO(rthellend): Consider adding support for Symlink.
}
}