gopkg.in/juju/charm.v6-unstable@v6.0.0-20171026192109-50d0c219b496/charmdir.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  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"path/filepath"
    13  	"strconv"
    14  	"strings"
    15  	"syscall"
    16  )
    17  
    18  // The CharmDir type encapsulates access to data and operations
    19  // on a charm directory.
    20  type CharmDir struct {
    21  	Path     string
    22  	meta     *Meta
    23  	config   *Config
    24  	metrics  *Metrics
    25  	actions  *Actions
    26  	revision int
    27  }
    28  
    29  // Trick to ensure *CharmDir implements the Charm interface.
    30  var _ Charm = (*CharmDir)(nil)
    31  
    32  // IsCharmDir report whether the path is likely to represent
    33  // a charm, even it may be incomplete.
    34  func IsCharmDir(path string) bool {
    35  	dir := &CharmDir{Path: path}
    36  	_, err := os.Stat(dir.join("metadata.yaml"))
    37  	return err == nil
    38  }
    39  
    40  // ReadCharmDir returns a CharmDir representing an expanded charm directory.
    41  func ReadCharmDir(path string) (dir *CharmDir, err error) {
    42  	dir = &CharmDir{Path: path}
    43  	file, err := os.Open(dir.join("metadata.yaml"))
    44  	if err != nil {
    45  		return nil, err
    46  	}
    47  	dir.meta, err = ReadMeta(file)
    48  	file.Close()
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  
    53  	file, err = os.Open(dir.join("config.yaml"))
    54  	if _, ok := err.(*os.PathError); ok {
    55  		dir.config = NewConfig()
    56  	} else if err != nil {
    57  		return nil, err
    58  	} else {
    59  		dir.config, err = ReadConfig(file)
    60  		file.Close()
    61  		if err != nil {
    62  			return nil, err
    63  		}
    64  	}
    65  
    66  	file, err = os.Open(dir.join("metrics.yaml"))
    67  	if err == nil {
    68  		dir.metrics, err = ReadMetrics(file)
    69  		file.Close()
    70  		if err != nil {
    71  			return nil, err
    72  		}
    73  	} else if !os.IsNotExist(err) {
    74  		return nil, err
    75  	}
    76  
    77  	file, err = os.Open(dir.join("actions.yaml"))
    78  	if _, ok := err.(*os.PathError); ok {
    79  		dir.actions = NewActions()
    80  	} else if err != nil {
    81  		return nil, err
    82  	} else {
    83  		dir.actions, err = ReadActionsYaml(file)
    84  		file.Close()
    85  		if err != nil {
    86  			return nil, err
    87  		}
    88  	}
    89  
    90  	if file, err = os.Open(dir.join("revision")); err == nil {
    91  		_, err = fmt.Fscan(file, &dir.revision)
    92  		file.Close()
    93  		if err != nil {
    94  			return nil, errors.New("invalid revision file")
    95  		}
    96  	}
    97  
    98  	return dir, nil
    99  }
   100  
   101  // join builds a path rooted at the charm's expanded directory
   102  // path and the extra path components provided.
   103  func (dir *CharmDir) join(parts ...string) string {
   104  	parts = append([]string{dir.Path}, parts...)
   105  	return filepath.Join(parts...)
   106  }
   107  
   108  // Revision returns the revision number for the charm
   109  // expanded in dir.
   110  func (dir *CharmDir) Revision() int {
   111  	return dir.revision
   112  }
   113  
   114  // Meta returns the Meta representing the metadata.yaml file
   115  // for the charm expanded in dir.
   116  func (dir *CharmDir) Meta() *Meta {
   117  	return dir.meta
   118  }
   119  
   120  // Config returns the Config representing the config.yaml file
   121  // for the charm expanded in dir.
   122  func (dir *CharmDir) Config() *Config {
   123  	return dir.config
   124  }
   125  
   126  // Metrics returns the Metrics representing the metrics.yaml file
   127  // for the charm expanded in dir.
   128  func (dir *CharmDir) Metrics() *Metrics {
   129  	return dir.metrics
   130  }
   131  
   132  // Actions returns the Actions representing the actions.yaml file
   133  // for the charm expanded in dir.
   134  func (dir *CharmDir) Actions() *Actions {
   135  	return dir.actions
   136  }
   137  
   138  // SetRevision changes the charm revision number. This affects
   139  // the revision reported by Revision and the revision of the
   140  // charm archived by ArchiveTo.
   141  // The revision file in the charm directory is not modified.
   142  func (dir *CharmDir) SetRevision(revision int) {
   143  	dir.revision = revision
   144  }
   145  
   146  // SetDiskRevision does the same as SetRevision but also changes
   147  // the revision file in the charm directory.
   148  func (dir *CharmDir) SetDiskRevision(revision int) error {
   149  	dir.SetRevision(revision)
   150  	file, err := os.OpenFile(dir.join("revision"), os.O_WRONLY|os.O_CREATE, 0644)
   151  	if err != nil {
   152  		return err
   153  	}
   154  	_, err = file.Write([]byte(strconv.Itoa(revision)))
   155  	file.Close()
   156  	return err
   157  }
   158  
   159  // resolveSymlinkedRoot returns the target destination of a
   160  // charm root directory if the root directory is a symlink.
   161  func resolveSymlinkedRoot(rootPath string) (string, error) {
   162  	info, err := os.Lstat(rootPath)
   163  	if err == nil && info.Mode()&os.ModeSymlink != 0 {
   164  		rootPath, err = filepath.EvalSymlinks(rootPath)
   165  		if err != nil {
   166  			return "", fmt.Errorf("cannot read path symlink at %q: %v", rootPath, err)
   167  		}
   168  	}
   169  	return rootPath, nil
   170  }
   171  
   172  // ArchiveTo creates a charm file from the charm expanded in dir.
   173  // By convention a charm archive should have a ".charm" suffix.
   174  func (dir *CharmDir) ArchiveTo(w io.Writer) error {
   175  	return writeArchive(w, dir.Path, dir.revision, dir.Meta().Hooks())
   176  }
   177  
   178  func writeArchive(w io.Writer, path string, revision int, hooks map[string]bool) error {
   179  	zipw := zip.NewWriter(w)
   180  	defer zipw.Close()
   181  
   182  	// The root directory may be symlinked elsewhere so
   183  	// resolve that before creating the zip.
   184  	rootPath, err := resolveSymlinkedRoot(path)
   185  	if err != nil {
   186  		return err
   187  	}
   188  	zp := zipPacker{zipw, rootPath, hooks}
   189  	if revision != -1 {
   190  		zp.AddRevision(revision)
   191  	}
   192  	return filepath.Walk(rootPath, zp.WalkFunc())
   193  }
   194  
   195  type zipPacker struct {
   196  	*zip.Writer
   197  	root  string
   198  	hooks map[string]bool
   199  }
   200  
   201  func (zp *zipPacker) WalkFunc() filepath.WalkFunc {
   202  	return func(path string, fi os.FileInfo, err error) error {
   203  		return zp.visit(path, fi, err)
   204  	}
   205  }
   206  
   207  func (zp *zipPacker) AddRevision(revision int) error {
   208  	h := &zip.FileHeader{Name: "revision"}
   209  	h.SetMode(syscall.S_IFREG | 0644)
   210  	w, err := zp.CreateHeader(h)
   211  	if err == nil {
   212  		_, err = w.Write([]byte(strconv.Itoa(revision)))
   213  	}
   214  	return err
   215  }
   216  
   217  func (zp *zipPacker) visit(path string, fi os.FileInfo, err error) error {
   218  	if err != nil {
   219  		return err
   220  	}
   221  	relpath, err := filepath.Rel(zp.root, path)
   222  	if err != nil {
   223  		return err
   224  	}
   225  	method := zip.Deflate
   226  	hidden := len(relpath) > 1 && relpath[0] == '.'
   227  	if fi.IsDir() {
   228  		if relpath == "build" {
   229  			return filepath.SkipDir
   230  		}
   231  		if hidden {
   232  			return filepath.SkipDir
   233  		}
   234  		relpath += "/"
   235  		method = zip.Store
   236  	}
   237  
   238  	mode := fi.Mode()
   239  	if err := checkFileType(relpath, mode); err != nil {
   240  		return err
   241  	}
   242  	if mode&os.ModeSymlink != 0 {
   243  		method = zip.Store
   244  	}
   245  	if hidden || relpath == "revision" {
   246  		return nil
   247  	}
   248  	h := &zip.FileHeader{
   249  		Name:   relpath,
   250  		Method: method,
   251  	}
   252  
   253  	perm := os.FileMode(0644)
   254  	if mode&os.ModeSymlink != 0 {
   255  		perm = 0777
   256  	} else if mode&0100 != 0 {
   257  		perm = 0755
   258  	}
   259  	if filepath.Dir(relpath) == "hooks" {
   260  		hookName := filepath.Base(relpath)
   261  		if _, ok := zp.hooks[hookName]; ok && !fi.IsDir() && mode&0100 == 0 {
   262  			logger.Warningf("making %q executable in charm", path)
   263  			perm = perm | 0100
   264  		}
   265  	}
   266  	h.SetMode(mode&^0777 | perm)
   267  
   268  	w, err := zp.CreateHeader(h)
   269  	if err != nil || fi.IsDir() {
   270  		return err
   271  	}
   272  	var data []byte
   273  	if mode&os.ModeSymlink != 0 {
   274  		target, err := os.Readlink(path)
   275  		if err != nil {
   276  			return err
   277  		}
   278  		if err := checkSymlinkTarget(zp.root, relpath, target); err != nil {
   279  			return err
   280  		}
   281  		data = []byte(target)
   282  		_, err = w.Write(data)
   283  	} else {
   284  		file, err := os.Open(path)
   285  		if err != nil {
   286  			return err
   287  		}
   288  		defer file.Close()
   289  		_, err = io.Copy(w, file)
   290  	}
   291  	return err
   292  }
   293  
   294  func checkSymlinkTarget(basedir, symlink, target string) error {
   295  	if filepath.IsAbs(target) {
   296  		return fmt.Errorf("symlink %q is absolute: %q", symlink, target)
   297  	}
   298  	p := filepath.Join(filepath.Dir(symlink), target)
   299  	if p == ".." || strings.HasPrefix(p, "../") {
   300  		return fmt.Errorf("symlink %q links out of charm: %q", symlink, target)
   301  	}
   302  	return nil
   303  }
   304  
   305  func checkFileType(path string, mode os.FileMode) error {
   306  	e := "file has an unknown type: %q"
   307  	switch mode & os.ModeType {
   308  	case os.ModeDir, os.ModeSymlink, 0:
   309  		return nil
   310  	case os.ModeNamedPipe:
   311  		e = "file is a named pipe: %q"
   312  	case os.ModeSocket:
   313  		e = "file is a socket: %q"
   314  	case os.ModeDevice:
   315  		e = "file is a device: %q"
   316  	}
   317  	return fmt.Errorf(e, path)
   318  }