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 }