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