launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/charm/bundle.go (about)

     1  // Copyright 2011, 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package charm
     5  
     6  import (
     7  	"archive/zip"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"io/ioutil"
    12  	"os"
    13  	"path/filepath"
    14  	"strconv"
    15  	"strings"
    16  )
    17  
    18  // The Bundle type encapsulates access to data and operations
    19  // on a charm bundle.
    20  type Bundle struct {
    21  	Path     string // May be empty if Bundle wasn't read from a file
    22  	meta     *Meta
    23  	config   *Config
    24  	revision int
    25  	r        io.ReaderAt
    26  	size     int64
    27  }
    28  
    29  // Trick to ensure *Bundle implements the Charm interface.
    30  var _ Charm = (*Bundle)(nil)
    31  
    32  // ReadBundle returns a Bundle for the charm in path.
    33  func ReadBundle(path string) (bundle *Bundle, err error) {
    34  	f, err := os.Open(path)
    35  	if err != nil {
    36  		return
    37  	}
    38  	defer f.Close()
    39  	fi, err := f.Stat()
    40  	if err != nil {
    41  		return
    42  	}
    43  	b, err := readBundle(f, fi.Size())
    44  	if err != nil {
    45  		return
    46  	}
    47  	b.Path = path
    48  	return b, nil
    49  }
    50  
    51  // ReadBundleBytes returns a Bundle read from the given data.
    52  // Make sure the bundle fits in memory before using this.
    53  func ReadBundleBytes(data []byte) (bundle *Bundle, err error) {
    54  	return readBundle(readAtBytes(data), int64(len(data)))
    55  }
    56  
    57  func readBundle(r io.ReaderAt, size int64) (bundle *Bundle, err error) {
    58  	b := &Bundle{r: r, size: size}
    59  	zipr, err := zip.NewReader(r, size)
    60  	if err != nil {
    61  		return
    62  	}
    63  	reader, err := zipOpen(zipr, "metadata.yaml")
    64  	if err != nil {
    65  		return
    66  	}
    67  	b.meta, err = ReadMeta(reader)
    68  	reader.Close()
    69  	if err != nil {
    70  		return
    71  	}
    72  
    73  	reader, err = zipOpen(zipr, "config.yaml")
    74  	if _, ok := err.(*noBundleFile); ok {
    75  		b.config = NewConfig()
    76  	} else if err != nil {
    77  		return nil, err
    78  	} else {
    79  		b.config, err = ReadConfig(reader)
    80  		reader.Close()
    81  		if err != nil {
    82  			return nil, err
    83  		}
    84  	}
    85  
    86  	reader, err = zipOpen(zipr, "revision")
    87  	if err != nil {
    88  		if _, ok := err.(*noBundleFile); !ok {
    89  			return
    90  		}
    91  		b.revision = b.meta.OldRevision
    92  	} else {
    93  		_, err = fmt.Fscan(reader, &b.revision)
    94  		if err != nil {
    95  			return nil, errors.New("invalid revision file")
    96  		}
    97  	}
    98  
    99  	return b, nil
   100  }
   101  
   102  func zipOpen(zipr *zip.Reader, path string) (rc io.ReadCloser, err error) {
   103  	for _, fh := range zipr.File {
   104  		if fh.Name == path {
   105  			return fh.Open()
   106  		}
   107  	}
   108  	return nil, &noBundleFile{path}
   109  }
   110  
   111  type noBundleFile struct {
   112  	path string
   113  }
   114  
   115  func (err noBundleFile) Error() string {
   116  	return fmt.Sprintf("bundle file not found: %s", err.path)
   117  }
   118  
   119  // Revision returns the revision number for the charm
   120  // expanded in dir.
   121  func (b *Bundle) Revision() int {
   122  	return b.revision
   123  }
   124  
   125  // SetRevision changes the charm revision number. This affects the
   126  // revision reported by Revision and the revision of the charm
   127  // directory created by ExpandTo.
   128  func (b *Bundle) SetRevision(revision int) {
   129  	b.revision = revision
   130  }
   131  
   132  // Meta returns the Meta representing the metadata.yaml file from bundle.
   133  func (b *Bundle) Meta() *Meta {
   134  	return b.meta
   135  }
   136  
   137  // Config returns the Config representing the config.yaml file
   138  // for the charm bundle.
   139  func (b *Bundle) Config() *Config {
   140  	return b.config
   141  }
   142  
   143  // ExpandTo expands the charm bundle into dir, creating it if necessary.
   144  // If any errors occur during the expansion procedure, the process will
   145  // continue. Only the last error found is returned.
   146  func (b *Bundle) ExpandTo(dir string) (err error) {
   147  	// If we have a Path, reopen the file. Otherwise, try to use
   148  	// the original ReaderAt.
   149  	r := b.r
   150  	size := b.size
   151  	if b.Path != "" {
   152  		f, err := os.Open(b.Path)
   153  		if err != nil {
   154  			return err
   155  		}
   156  		defer f.Close()
   157  		fi, err := f.Stat()
   158  		if err != nil {
   159  			return err
   160  		}
   161  		r = f
   162  		size = fi.Size()
   163  	}
   164  
   165  	zipr, err := zip.NewReader(r, size)
   166  	if err != nil {
   167  		return err
   168  	}
   169  
   170  	hooks := b.meta.Hooks()
   171  	var lasterr error
   172  	for _, zfile := range zipr.File {
   173  		if err := b.expand(hooks, dir, zfile); err != nil {
   174  			lasterr = err
   175  		}
   176  	}
   177  
   178  	revFile, err := os.Create(filepath.Join(dir, "revision"))
   179  	if err != nil {
   180  		return err
   181  	}
   182  	_, err = revFile.Write([]byte(strconv.Itoa(b.revision)))
   183  	revFile.Close()
   184  	if err != nil {
   185  		return err
   186  	}
   187  	return lasterr
   188  }
   189  
   190  // expand unpacks a charm's zip file into the given directory.
   191  // The hooks map holds all the possible hook names in the
   192  // charm.
   193  func (b *Bundle) expand(hooks map[string]bool, dir string, zfile *zip.File) error {
   194  	cleanName := filepath.Clean(zfile.Name)
   195  	if cleanName == "revision" {
   196  		return nil
   197  	}
   198  
   199  	r, err := zfile.Open()
   200  	if err != nil {
   201  		return err
   202  	}
   203  	defer r.Close()
   204  
   205  	mode := zfile.Mode()
   206  	path := filepath.Join(dir, cleanName)
   207  	if strings.HasSuffix(zfile.Name, "/") || mode&os.ModeDir != 0 {
   208  		err = os.MkdirAll(path, mode&0777)
   209  		if err != nil {
   210  			return err
   211  		}
   212  		return nil
   213  	}
   214  
   215  	base, _ := filepath.Split(path)
   216  	err = os.MkdirAll(base, 0755)
   217  	if err != nil {
   218  		return err
   219  	}
   220  
   221  	if mode&os.ModeSymlink != 0 {
   222  		data, err := ioutil.ReadAll(r)
   223  		if err != nil {
   224  			return err
   225  		}
   226  		target := string(data)
   227  		if err := checkSymlinkTarget(dir, cleanName, target); err != nil {
   228  			return err
   229  		}
   230  		return os.Symlink(target, path)
   231  	}
   232  	if filepath.Dir(cleanName) == "hooks" {
   233  		hookName := filepath.Base(cleanName)
   234  		if _, ok := hooks[hookName]; mode&os.ModeType == 0 && ok {
   235  			// Set all hooks executable (by owner)
   236  			mode = mode | 0100
   237  		}
   238  	}
   239  
   240  	if err := checkFileType(cleanName, mode); err != nil {
   241  		return err
   242  	}
   243  
   244  	f, err := os.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode&0777)
   245  	if err != nil {
   246  		return err
   247  	}
   248  	_, err = io.Copy(f, r)
   249  	f.Close()
   250  	return err
   251  }
   252  
   253  func checkSymlinkTarget(basedir, symlink, target string) error {
   254  	if filepath.IsAbs(target) {
   255  		return fmt.Errorf("symlink %q is absolute: %q", symlink, target)
   256  	}
   257  	p := filepath.Join(filepath.Dir(symlink), target)
   258  	if p == ".." || strings.HasPrefix(p, "../") {
   259  		return fmt.Errorf("symlink %q links out of charm: %q", symlink, target)
   260  	}
   261  	return nil
   262  }
   263  
   264  // FWIW, being able to do this is awesome.
   265  type readAtBytes []byte
   266  
   267  func (b readAtBytes) ReadAt(out []byte, off int64) (n int, err error) {
   268  	return copy(out, b[off:]), nil
   269  }