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