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 }