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