blob: 15b253bfff2dc7941dd844208ab08f56d5e4da1d [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package packages provides functionality to install ZIP and TAR packages.
package packages
import (
"archive/tar"
"archive/zip"
"compress/bzip2"
"compress/gzip"
"encoding/json"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"v.io/v23/services/repository"
"v.io/v23/verror"
)
const (
defaultType = "application/octet-stream"
createDirMode = 0755
createFileMode = 0644
)
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"},
}
const pkgPath = "v.io/x/ref/services/internal/packages"
var (
errBadMediaType = verror.Register(pkgPath+".errBadMediaType", verror.NoRetry, "{1:}{2:} unsupported media type{:_}")
errMkDirFailed = verror.Register(pkgPath+".errMkDirFailed", verror.NoRetry, "{1:}{2:} os.Mkdir({3}) failed{:_}")
errFailedToExtract = verror.Register(pkgPath+".errFailedToExtract", verror.NoRetry, "{1:}{2:} failed to extract file {3} outside of install directory{:_}")
errBadFileSize = verror.Register(pkgPath+".errBadFileSize", verror.NoRetry, "{1:}{2:} file size doesn't match for {3}: {4} != {5}{:_}")
errBadEncoding = verror.Register(pkgPath+".errBadEncoding", verror.NoRetry, "{1:}{2:} unsupported encoding{:_}")
)
// MediaInfoFile returns the name of the file where the media info is stored for
// the given package file.
func MediaInfoFile(pkgFile string) string {
const mediaInfoFileSuffix = ".__info"
return pkgFile + mediaInfoFileSuffix
}
// 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}
}
func copyFile(src, dst string) error {
s, err := os.Open(src)
if err != nil {
return err
}
defer s.Close()
d, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, createFileMode)
if err != nil {
return err
}
defer d.Close()
if _, err = io.Copy(d, s); err != nil {
return err
}
return d.Sync()
}
// Install installs a package in the given destination. If the package is a TAR
// or ZIP archive, the destination becomes a directory where the archive content
// is extracted. Otherwise, the destination is hard-linked to the package (or
// copied if hard link is not possible).
func Install(pkgFile, destination string) error {
mediaInfo, err := LoadMediaInfo(pkgFile)
if err != nil {
return err
}
switch mediaInfo.Type {
case "application/x-tar":
return extractTar(pkgFile, mediaInfo.Encoding, destination)
case "application/zip":
return extractZip(pkgFile, destination)
case defaultType, "text/plain":
if err := os.Link(pkgFile, destination); err != nil {
// Can't create hard link (e.g., different filesystem).
return copyFile(pkgFile, destination)
}
return nil
default:
// TODO(caprita): Instead of throwing an error, why not just
// handle things with os.Link(pkgFile, destination) as the two
// cases above?
return verror.New(errBadMediaType, nil, mediaInfo.Type)
}
}
// LoadMediaInfo returns the MediaInfo for the given package file.
func LoadMediaInfo(pkgFile string) (repository.MediaInfo, error) {
jInfo, err := ioutil.ReadFile(MediaInfoFile(pkgFile))
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 := MediaInfoFile(pkgFile)
if err := ioutil.WriteFile(infoFile, jInfo, os.FileMode(0600)); err != nil {
return err
}
return nil
}
// CreateZip creates a package from the files in the source directory. The
// created package is a Zip file.
func CreateZip(zipFile, sourceDir string) error {
z, err := os.OpenFile(zipFile, os.O_CREATE|os.O_WRONLY, os.FileMode(0644))
if err != nil {
return err
}
defer z.Close()
w := zip.NewWriter(z)
if err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if sourceDir == path {
return nil
}
fh, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
fh.Method = zip.Deflate
fh.Name, _ = filepath.Rel(sourceDir, path)
hdr, err := w.CreateHeader(fh)
if err != nil {
return err
}
if !info.IsDir() {
content, err := ioutil.ReadFile(path)
if err != nil {
return err
}
if _, err = hdr.Write(content); err != nil {
return err
}
}
return nil
}); err != nil {
return err
}
if err := w.Close(); err != nil {
return err
}
if err := SaveMediaInfo(zipFile, repository.MediaInfo{Type: "application/zip"}); err != nil {
return err
}
return nil
}
func extractZip(zipFile, installDir string) error {
if err := os.Mkdir(installDir, os.FileMode(createDirMode)); err != nil {
return verror.New(errMkDirFailed, nil, installDir, err)
}
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 verror.New(errFailedToExtract, nil, file.Name)
}
if fi.IsDir() {
if err := os.MkdirAll(name, os.FileMode(createDirMode)); 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(createDirMode)); err != nil {
return err
}
out, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY, os.FileMode(createFileMode))
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 verror.New(errBadFileSize, nil, fi.Name(), nbytes, fi.Size())
}
}
return nil
}
func extractTar(pkgFile string, encoding string, installDir string) error {
if err := os.Mkdir(installDir, os.FileMode(createDirMode)); err != nil {
return verror.New(errMkDirFailed, nil, installDir, err)
}
f, err := os.Open(pkgFile)
if err != nil {
return err
}
defer f.Close()
var reader io.Reader
switch encoding {
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 verror.New(errBadEncoding, nil, encoding)
}
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 verror.New(errFailedToExtract, nil, hdr.Name)
}
// Regular file
if hdr.Typeflag == tar.TypeReg {
parentName := filepath.Dir(name)
if err := os.MkdirAll(parentName, os.FileMode(createDirMode)); err != nil {
return err
}
out, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY, os.FileMode(createFileMode))
if err != nil {
return err
}
nbytes, err := io.Copy(out, tr)
out.Close()
if err != nil {
return err
}
if nbytes != hdr.Size {
return verror.New(errBadFileSize, nil, hdr.Name, nbytes, hdr.Size)
}
continue
}
// Directory
if hdr.Typeflag == tar.TypeDir {
if err := os.MkdirAll(name, os.FileMode(createDirMode)); err != nil && !os.IsExist(err) {
return err
}
continue
}
// Skip unsupported types
// TODO(rthellend): Consider adding support for Symlink.
}
}