gopkg.in/juju/charm.v6-unstable@v6.0.0-20171026192109-50d0c219b496/charmarchive.go (about)

     1  // Copyright 2011, 2012, 2013 Canonical Ltd.
     2  // Licensed under the LGPLv3, see LICENCE file for details.
     3  
     4  package charm
     5  
     6  import (
     7  	"archive/zip"
     8  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"os"
    14  	"path/filepath"
    15  	"strconv"
    16  
    17  	"github.com/juju/utils/set"
    18  	ziputil "github.com/juju/utils/zip"
    19  )
    20  
    21  // The CharmArchive type encapsulates access to data and operations
    22  // on a charm archive.
    23  type CharmArchive struct {
    24  	zopen zipOpener
    25  
    26  	Path     string // May be empty if CharmArchive wasn't read from a file
    27  	meta     *Meta
    28  	config   *Config
    29  	metrics  *Metrics
    30  	actions  *Actions
    31  	revision int
    32  }
    33  
    34  // Trick to ensure *CharmArchive implements the Charm interface.
    35  var _ Charm = (*CharmArchive)(nil)
    36  
    37  // ReadCharmArchive returns a CharmArchive for the charm in path.
    38  func ReadCharmArchive(path string) (*CharmArchive, error) {
    39  	a, err := readCharmArchive(newZipOpenerFromPath(path))
    40  	if err != nil {
    41  		return nil, err
    42  	}
    43  	a.Path = path
    44  	return a, nil
    45  }
    46  
    47  // ReadCharmArchiveBytes returns a CharmArchive read from the given data.
    48  // Make sure the archive fits in memory before using this.
    49  func ReadCharmArchiveBytes(data []byte) (archive *CharmArchive, err error) {
    50  	zopener := newZipOpenerFromReader(bytes.NewReader(data), int64(len(data)))
    51  	return readCharmArchive(zopener)
    52  }
    53  
    54  // ReadCharmArchiveFromReader returns a CharmArchive that uses
    55  // r to read the charm. The given size must hold the number
    56  // of available bytes in the file.
    57  //
    58  // Note that the caller is responsible for closing r - methods on
    59  // the returned CharmArchive may fail after that.
    60  func ReadCharmArchiveFromReader(r io.ReaderAt, size int64) (archive *CharmArchive, err error) {
    61  	return readCharmArchive(newZipOpenerFromReader(r, size))
    62  }
    63  
    64  func readCharmArchive(zopen zipOpener) (archive *CharmArchive, err error) {
    65  	b := &CharmArchive{
    66  		zopen: zopen,
    67  	}
    68  	zipr, err := zopen.openZip()
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  	defer zipr.Close()
    73  	reader, err := zipOpenFile(zipr, "metadata.yaml")
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  	b.meta, err = ReadMeta(reader)
    78  	reader.Close()
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  
    83  	reader, err = zipOpenFile(zipr, "config.yaml")
    84  	if _, ok := err.(*noCharmArchiveFile); ok {
    85  		b.config = NewConfig()
    86  	} else if err != nil {
    87  		return nil, err
    88  	} else {
    89  		b.config, err = ReadConfig(reader)
    90  		reader.Close()
    91  		if err != nil {
    92  			return nil, err
    93  		}
    94  	}
    95  
    96  	reader, err = zipOpenFile(zipr, "metrics.yaml")
    97  	if err == nil {
    98  		b.metrics, err = ReadMetrics(reader)
    99  		reader.Close()
   100  		if err != nil {
   101  			return nil, err
   102  		}
   103  	} else if _, ok := err.(*noCharmArchiveFile); !ok {
   104  		return nil, err
   105  	}
   106  
   107  	reader, err = zipOpenFile(zipr, "actions.yaml")
   108  	if _, ok := err.(*noCharmArchiveFile); ok {
   109  		b.actions = NewActions()
   110  	} else if err != nil {
   111  		return nil, err
   112  	} else {
   113  		b.actions, err = ReadActionsYaml(reader)
   114  		reader.Close()
   115  		if err != nil {
   116  			return nil, err
   117  		}
   118  	}
   119  
   120  	reader, err = zipOpenFile(zipr, "revision")
   121  	if err != nil {
   122  		if _, ok := err.(*noCharmArchiveFile); !ok {
   123  			return nil, err
   124  		}
   125  	} else {
   126  		_, err = fmt.Fscan(reader, &b.revision)
   127  		if err != nil {
   128  			return nil, errors.New("invalid revision file")
   129  		}
   130  	}
   131  
   132  	return b, nil
   133  }
   134  
   135  func zipOpenFile(zipr *zipReadCloser, path string) (rc io.ReadCloser, err error) {
   136  	for _, fh := range zipr.File {
   137  		if fh.Name == path {
   138  			return fh.Open()
   139  		}
   140  	}
   141  	return nil, &noCharmArchiveFile{path}
   142  }
   143  
   144  type noCharmArchiveFile struct {
   145  	path string
   146  }
   147  
   148  func (err noCharmArchiveFile) Error() string {
   149  	return fmt.Sprintf("archive file %q not found", err.path)
   150  }
   151  
   152  // Revision returns the revision number for the charm
   153  // expanded in dir.
   154  func (a *CharmArchive) Revision() int {
   155  	return a.revision
   156  }
   157  
   158  // SetRevision changes the charm revision number. This affects the
   159  // revision reported by Revision and the revision of the charm
   160  // directory created by ExpandTo.
   161  func (a *CharmArchive) SetRevision(revision int) {
   162  	a.revision = revision
   163  }
   164  
   165  // Meta returns the Meta representing the metadata.yaml file from archive.
   166  func (a *CharmArchive) Meta() *Meta {
   167  	return a.meta
   168  }
   169  
   170  // Config returns the Config representing the config.yaml file
   171  // for the charm archive.
   172  func (a *CharmArchive) Config() *Config {
   173  	return a.config
   174  }
   175  
   176  // Metrics returns the Metrics representing the metrics.yaml file
   177  // for the charm archive.
   178  func (a *CharmArchive) Metrics() *Metrics {
   179  	return a.metrics
   180  }
   181  
   182  // Actions returns the Actions map for the actions.yaml file for the charm
   183  // archive.
   184  func (a *CharmArchive) Actions() *Actions {
   185  	return a.actions
   186  }
   187  
   188  type zipReadCloser struct {
   189  	io.Closer
   190  	*zip.Reader
   191  }
   192  
   193  // zipOpener holds the information needed to open a zip
   194  // file.
   195  type zipOpener interface {
   196  	openZip() (*zipReadCloser, error)
   197  }
   198  
   199  // newZipOpenerFromPath returns a zipOpener that can be
   200  // used to read the archive from the given path.
   201  func newZipOpenerFromPath(path string) zipOpener {
   202  	return &zipPathOpener{path: path}
   203  }
   204  
   205  // newZipOpenerFromReader returns a zipOpener that can be
   206  // used to read the archive from the given ReaderAt
   207  // holding the given number of bytes.
   208  func newZipOpenerFromReader(r io.ReaderAt, size int64) zipOpener {
   209  	return &zipReaderOpener{
   210  		r:    r,
   211  		size: size,
   212  	}
   213  }
   214  
   215  type zipPathOpener struct {
   216  	path string
   217  }
   218  
   219  func (zo *zipPathOpener) openZip() (*zipReadCloser, error) {
   220  	f, err := os.Open(zo.path)
   221  	if err != nil {
   222  		return nil, err
   223  	}
   224  	fi, err := f.Stat()
   225  	if err != nil {
   226  		f.Close()
   227  		return nil, err
   228  	}
   229  	r, err := zip.NewReader(f, fi.Size())
   230  	if err != nil {
   231  		f.Close()
   232  		return nil, err
   233  	}
   234  	return &zipReadCloser{Closer: f, Reader: r}, nil
   235  }
   236  
   237  type zipReaderOpener struct {
   238  	r    io.ReaderAt
   239  	size int64
   240  }
   241  
   242  func (zo *zipReaderOpener) openZip() (*zipReadCloser, error) {
   243  	r, err := zip.NewReader(zo.r, zo.size)
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  	return &zipReadCloser{Closer: ioutil.NopCloser(nil), Reader: r}, nil
   248  }
   249  
   250  // Manifest returns a set of the charm's contents.
   251  func (a *CharmArchive) Manifest() (set.Strings, error) {
   252  	zipr, err := a.zopen.openZip()
   253  	if err != nil {
   254  		return set.NewStrings(), err
   255  	}
   256  	defer zipr.Close()
   257  	paths, err := ziputil.Find(zipr.Reader, "*")
   258  	if err != nil {
   259  		return set.NewStrings(), err
   260  	}
   261  	manifest := set.NewStrings(paths...)
   262  	// We always write out a revision file, even if there isn't one in the
   263  	// archive; and we always strip ".", because that's sometimes not present.
   264  	manifest.Add("revision")
   265  	manifest.Remove(".")
   266  	return manifest, nil
   267  }
   268  
   269  // ExpandTo expands the charm archive into dir, creating it if necessary.
   270  // If any errors occur during the expansion procedure, the process will
   271  // abort.
   272  func (a *CharmArchive) ExpandTo(dir string) error {
   273  	zipr, err := a.zopen.openZip()
   274  	if err != nil {
   275  		return err
   276  	}
   277  	defer zipr.Close()
   278  	if err := ziputil.ExtractAll(zipr.Reader, dir); err != nil {
   279  		return err
   280  	}
   281  	hooksDir := filepath.Join(dir, "hooks")
   282  	fixHook := fixHookFunc(hooksDir, a.meta.Hooks())
   283  	if err := filepath.Walk(hooksDir, fixHook); err != nil {
   284  		if !os.IsNotExist(err) {
   285  			return err
   286  		}
   287  	}
   288  	revFile, err := os.Create(filepath.Join(dir, "revision"))
   289  	if err != nil {
   290  		return err
   291  	}
   292  	if _, err := revFile.Write([]byte(strconv.Itoa(a.revision))); err != nil {
   293  		return err
   294  	}
   295  	if err := revFile.Sync(); err != nil {
   296  		return err
   297  	}
   298  	if err := revFile.Close(); err != nil {
   299  		return err
   300  	}
   301  	return nil
   302  }
   303  
   304  // fixHookFunc returns a WalkFunc that makes sure hooks are owner-executable.
   305  func fixHookFunc(hooksDir string, hookNames map[string]bool) filepath.WalkFunc {
   306  	return func(path string, info os.FileInfo, err error) error {
   307  		if err != nil {
   308  			return err
   309  		}
   310  		mode := info.Mode()
   311  		if path != hooksDir && mode.IsDir() {
   312  			return filepath.SkipDir
   313  		}
   314  		if name := filepath.Base(path); hookNames[name] {
   315  			if mode&0100 == 0 {
   316  				return os.Chmod(path, mode|0100)
   317  			}
   318  		}
   319  		return nil
   320  	}
   321  }